aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--build.gradle1
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java440
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java4
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Account.java4
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Contact.java182
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java13
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java263
7 files changed, 842 insertions, 65 deletions
diff --git a/build.gradle b/build.gradle
index 69eee6e5..d16fd3b8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -37,6 +37,7 @@ dependencies {
compile 'de.timroes.android:EnhancedListView:0.3.4'
compile 'me.leolin:ShortcutBadger:1.1.1@aar'
compile 'com.kyleduo.switchbutton:library:1.2.8'
+ compile 'org.whispersystems:axolotl-android:1.3.4'
}
android {
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
new file mode 100644
index 00000000..8e300248
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java
@@ -0,0 +1,440 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+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.InvalidKeyIdException;
+import org.whispersystems.libaxolotl.ecc.Curve;
+import org.whispersystems.libaxolotl.ecc.ECKeyPair;
+import org.whispersystems.libaxolotl.state.AxolotlStore;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SessionRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+import org.whispersystems.libaxolotl.util.KeyHelper;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xmpp.jid.InvalidJidException;
+import eu.siacs.conversations.xmpp.jid.Jid;
+
+public class AxolotlService {
+
+ private Account account;
+ private XmppConnectionService mXmppConnectionService;
+ private SQLiteAxolotlStore axolotlStore;
+ private Map<Jid,XmppAxolotlSession> sessions;
+
+ public static class SQLiteAxolotlStore implements AxolotlStore {
+
+ public static final String PREKEY_TABLENAME = "prekeys";
+ public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
+ public static final String SESSION_TABLENAME = "signed_prekeys";
+ public static final String NAME = "name";
+ public static final String DEVICE_ID = "device_id";
+ public static final String ID = "id";
+ public static final String KEY = "key";
+ public static final String ACCOUNT = "account";
+
+ public static final String JSONKEY_IDENTITY_KEY_PAIR = "axolotl_key";
+ public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
+
+ private final Account account;
+ private final XmppConnectionService mXmppConnectionService;
+
+ private final IdentityKeyPair identityKeyPair;
+ private final int localRegistrationId;
+
+
+ private static IdentityKeyPair generateIdentityKeyPair() {
+ Log.d(Config.LOGTAG, "Generating axolotl IdentityKeyPair...");
+ ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
+ IdentityKeyPair ownKey = new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
+ identityKeyPairKeys.getPrivateKey());
+ return ownKey;
+ }
+
+ private static int generateRegistrationId() {
+ Log.d(Config.LOGTAG, "Generating axolotl registration ID...");
+ int reg_id = KeyHelper.generateRegistrationId(false);
+ return reg_id;
+ }
+
+ public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
+ this.account = account;
+ this.mXmppConnectionService = service;
+ this.identityKeyPair = loadIdentityKeyPair();
+ this.localRegistrationId = loadRegistrationId();
+ }
+
+ // --------------------------------------
+ // IdentityKeyStore
+ // --------------------------------------
+
+ private IdentityKeyPair loadIdentityKeyPair() {
+ String serializedKey = this.account.getKey(JSONKEY_IDENTITY_KEY_PAIR);
+ IdentityKeyPair ownKey;
+ if( serializedKey != null ) {
+ try {
+ ownKey = new IdentityKeyPair(serializedKey.getBytes());
+ } catch (InvalidKeyException e) {
+ Log.d(Config.LOGTAG, "Invalid key stored for account " + account.getJid() + ": " + e.getMessage());
+ return null;
+ }
+ } else {
+ Log.d(Config.LOGTAG, "Could not retrieve axolotl key for account " + account.getJid());
+ ownKey = generateIdentityKeyPair();
+ boolean success = this.account.setKey(JSONKEY_IDENTITY_KEY_PAIR, new String(ownKey.serialize()));
+ if(success) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } else {
+ Log.e(Config.LOGTAG, "Failed to write new key to the database!");
+ }
+ }
+ return ownKey;
+ }
+
+ private int loadRegistrationId() {
+ String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
+ int reg_id;
+ if (regIdString != null) {
+ reg_id = Integer.valueOf(regIdString);
+ } else {
+ Log.d(Config.LOGTAG, "Could not retrieve axolotl registration id for account " + account.getJid());
+ reg_id = generateRegistrationId();
+ boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID,""+reg_id);
+ if(success) {
+ mXmppConnectionService.databaseBackend.updateAccount(account);
+ } else {
+ Log.e(Config.LOGTAG, "Failed to write new key to the database!");
+ }
+ }
+ return reg_id;
+ }
+
+ /**
+ * Get the local client's identity key pair.
+ *
+ * @return The local client's persistent identity key pair.
+ */
+ @Override
+ public IdentityKeyPair getIdentityKeyPair() {
+ return identityKeyPair;
+ }
+
+ /**
+ * Return the local client's registration ID.
+ * <p/>
+ * Clients should maintain a registration ID, a random number
+ * between 1 and 16380 that's generated once at install time.
+ *
+ * @return the local client's registration ID.
+ */
+ @Override
+ public int getLocalRegistrationId() {
+ return localRegistrationId;
+ }
+
+ /**
+ * Save a remote client's identity key
+ * <p/>
+ * Store a remote client's identity key as trusted.
+ *
+ * @param name The name of the remote client.
+ * @param identityKey The remote client's identity key.
+ */
+ @Override
+ public void saveIdentity(String name, IdentityKey identityKey) {
+ try {
+ Jid contactJid = Jid.fromString(name);
+ Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid);
+ if (conversation != null) {
+ conversation.getContact().addAxolotlIdentityKey(identityKey, false);
+ mXmppConnectionService.updateConversationUi();
+ mXmppConnectionService.syncRosterToDisk(conversation.getAccount());
+ }
+ } catch (final InvalidJidException e) {
+ Log.e(Config.LOGTAG, "Failed to save identityKey for contact name " + name + ": " + e.toString());
+ }
+ }
+
+ /**
+ * Verify a remote client's identity key.
+ * <p/>
+ * Determine whether a remote client's identity is trusted. Convention is
+ * that the TextSecure protocol is 'trust on first use.' This means that
+ * an identity key is considered 'trusted' if there is no entry for the recipient
+ * in the local store, or if it matches the saved key for a recipient in the local
+ * store. Only if it mismatches an entry in the local store is it considered
+ * 'untrusted.'
+ *
+ * @param name The name of the remote client.
+ * @param identityKey The identity key to verify.
+ * @return true if trusted, false if untrusted.
+ */
+ @Override
+ public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
+ try {
+ Jid contactJid = Jid.fromString(name);
+ Conversation conversation = this.mXmppConnectionService.find(this.account, contactJid);
+ if (conversation != null) {
+ List<IdentityKey> trustedKeys = conversation.getContact().getTrustedAxolotlIdentityKeys();
+ return trustedKeys.contains(identityKey);
+ } else {
+ return false;
+ }
+ } catch (final InvalidJidException e) {
+ Log.e(Config.LOGTAG, "Failed to save identityKey for contact name" + name + ": " + e.toString());
+ return false;
+ }
+ }
+
+ // --------------------------------------
+ // SessionStore
+ // --------------------------------------
+
+ /**
+ * Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
+ * or a new SessionRecord if one does not currently exist.
+ * <p/>
+ * It is important that implementations return a copy of the current durable information. The
+ * returned SessionRecord may be modified, but those changes should not have an effect on the
+ * durable session state (what is returned by subsequent calls to this method) without the
+ * store method being called here first.
+ *
+ * @param address The name and device ID of the remote client.
+ * @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
+ * a new SessionRecord if one does not currently exist.
+ */
+ @Override
+ public SessionRecord loadSession(AxolotlAddress address) {
+ SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
+ return (session!=null)?session:new SessionRecord();
+ }
+
+ /**
+ * Returns all known devices with active sessions for a recipient
+ *
+ * @param name the name of the client.
+ * @return all known sub-devices with active sessions.
+ */
+ @Override
+ public List<Integer> getSubDeviceSessions(String name) {
+ return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
+ new AxolotlAddress(name,0));
+ }
+
+ /**
+ * Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ * @param record the current SessionRecord for the remote client.
+ */
+ @Override
+ public void storeSession(AxolotlAddress address, SessionRecord record) {
+ mXmppConnectionService.databaseBackend.storeSession(account, address, record);
+ }
+
+ /**
+ * Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ * @return true if a {@link SessionRecord} exists, false otherwise.
+ */
+ @Override
+ public boolean containsSession(AxolotlAddress address) {
+ return mXmppConnectionService.databaseBackend.containsSession(account, address);
+ }
+
+ /**
+ * Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
+ *
+ * @param address the address of the remote client.
+ */
+ @Override
+ public void deleteSession(AxolotlAddress address) {
+ mXmppConnectionService.databaseBackend.deleteSession(account, address);
+ }
+
+ /**
+ * Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
+ *
+ * @param name the name of the remote client.
+ */
+ @Override
+ public void deleteAllSessions(String name) {
+ mXmppConnectionService.databaseBackend.deleteAllSessions(account,
+ new AxolotlAddress(name,0));
+ }
+
+ // --------------------------------------
+ // PreKeyStore
+ // --------------------------------------
+
+ /**
+ * Load a local PreKeyRecord.
+ *
+ * @param preKeyId the ID of the local PreKeyRecord.
+ * @return the corresponding PreKeyRecord.
+ * @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
+ */
+ @Override
+ public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
+ PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
+ if(record == null) {
+ throw new InvalidKeyIdException("No such PreKeyRecord!");
+ }
+ return record;
+ }
+
+ /**
+ * Store a local PreKeyRecord.
+ *
+ * @param preKeyId the ID of the PreKeyRecord to store.
+ * @param record the PreKeyRecord.
+ */
+ @Override
+ public void storePreKey(int preKeyId, PreKeyRecord record) {
+ mXmppConnectionService.databaseBackend.storePreKey(account, record);
+ }
+
+ /**
+ * @param preKeyId A PreKeyRecord ID.
+ * @return true if the store has a record for the preKeyId, otherwise false.
+ */
+ @Override
+ public boolean containsPreKey(int preKeyId) {
+ return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
+ }
+
+ /**
+ * Delete a PreKeyRecord from local storage.
+ *
+ * @param preKeyId The ID of the PreKeyRecord to remove.
+ */
+ @Override
+ public void removePreKey(int preKeyId) {
+ mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
+ }
+
+ // --------------------------------------
+ // SignedPreKeyStore
+ // --------------------------------------
+
+ /**
+ * Load a local SignedPreKeyRecord.
+ *
+ * @param signedPreKeyId the ID of the local SignedPreKeyRecord.
+ * @return the corresponding SignedPreKeyRecord.
+ * @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
+ */
+ @Override
+ public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
+ SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
+ if(record == null) {
+ throw new InvalidKeyIdException("No such PreKeyRecord!");
+ }
+ return record;
+ }
+
+ /**
+ * Load all local SignedPreKeyRecords.
+ *
+ * @return All stored SignedPreKeyRecords.
+ */
+ @Override
+ public List<SignedPreKeyRecord> loadSignedPreKeys() {
+ return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
+ }
+
+ /**
+ * Store a local SignedPreKeyRecord.
+ *
+ * @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
+ * @param record the SignedPreKeyRecord.
+ */
+ @Override
+ public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
+ mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
+ }
+
+ /**
+ * @param signedPreKeyId A SignedPreKeyRecord ID.
+ * @return true if the store has a record for the signedPreKeyId, otherwise false.
+ */
+ @Override
+ public boolean containsSignedPreKey(int signedPreKeyId) {
+ return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
+ }
+
+ /**
+ * Delete a SignedPreKeyRecord from local storage.
+ *
+ * @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
+ */
+ @Override
+ public void removeSignedPreKey(int signedPreKeyId) {
+ mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
+ }
+ }
+
+ private static class XmppAxolotlSession {
+ private List<Message> untrustedMessages;
+ private AxolotlStore axolotlStore;
+
+ public XmppAxolotlSession(SQLiteAxolotlStore axolotlStore) {
+ this.untrustedMessages = new ArrayList<>();
+ this.axolotlStore = axolotlStore;
+ }
+
+ public void trust() {
+ for (Message message : this.untrustedMessages) {
+ message.trust();
+ }
+ this.untrustedMessages = null;
+ }
+
+ public boolean isTrusted() {
+ return (this.untrustedMessages == null);
+ }
+
+ public String processReceiving(XmppAxolotlMessage incomingMessage) {
+ return null;
+ }
+
+ public XmppAxolotlMessage processSending(String outgoingMessage) {
+ return null;
+ }
+ }
+
+ public AxolotlService(Account account, XmppConnectionService connectionService) {
+ this.mXmppConnectionService = connectionService;
+ this.account = account;
+ this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
+ this.sessions = new HashMap<>();
+ }
+
+ public void trustSession(Jid counterpart) {
+ XmppAxolotlSession session = sessions.get(counterpart);
+ if(session != null) {
+ session.trust();
+ }
+ }
+
+ public boolean isTrustedSession(Jid counterpart) {
+ XmppAxolotlSession session = sessions.get(counterpart);
+ return session != null && session.isTrusted();
+ }
+
+
+}
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
new file mode 100644
index 00000000..b11670e4
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java
@@ -0,0 +1,4 @@
+package eu.siacs.conversations.crypto.axolotl;
+
+public class XmppAxolotlMessage {
+}
diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java
index f472361f..38312566 100644
--- a/src/main/java/eu/siacs/conversations/entities/Account.java
+++ b/src/main/java/eu/siacs/conversations/entities/Account.java
@@ -254,6 +254,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);
diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java
index e546f214..2f9b375d 100644
--- a/src/main/java/eu/siacs/conversations/entities/Contact.java
+++ b/src/main/java/eu/siacs/conversations/entities/Contact.java
@@ -2,15 +2,19 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
+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 +187,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,63 +287,109 @@ 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;
+ 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;
+ }
}
- try {
- JSONArray fingerprints;
- if (!this.keys.has("otr_fingerprints")) {
- fingerprints = new JSONArray();
+ }
+ public long getPgpKeyId() {
+ synchronized (this.keys) {
+ if (this.keys.has("pgp_keyid")) {
+ try {
+ return this.keys.getLong("pgp_keyid");
+ } catch (JSONException e) {
+ return 0;
+ }
} else {
- fingerprints = this.keys.getJSONArray("otr_fingerprints");
+ return 0;
}
- 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")) {
+ public void setPgpKeyId(long keyId) {
+ synchronized (this.keys) {
try {
- return this.keys.getLong("pgp_keyid");
- } catch (JSONException e) {
- return 0;
+ this.keys.put("pgp_keyid", keyId);
+ } catch (final JSONException ignored) {
}
- } else {
- return 0;
}
}
- public void setPgpKeyId(long keyId) {
- try {
- this.keys.put("pgp_keyid", keyId);
- } catch (final JSONException ignored) {
+ public List<IdentityKey> getTrustedAxolotlIdentityKeys() {
+ synchronized (this.keys) {
+ JSONArray serializedKeyItems = this.keys.optJSONArray("axolotl_identity_key");
+ List<IdentityKey> identityKeys = new ArrayList<>();
+ if(serializedKeyItems != null) {
+ for(int i = 0; i<serializedKeyItems.length();++i) {
+ try {
+ String serializedKeyItem = serializedKeyItems.getString(i);
+ IdentityKey identityKey = new IdentityKey(serializedKeyItem.getBytes(), 0);
+ identityKeys.add(identityKey);
+ } catch (InvalidKeyException e) {
+ Log.e(Config.LOGTAG, "Invalid axolotl identity key encountered at" + this.getJid() + ": " + e.getMessage());
+ } catch (JSONException e) {
+ Log.e(Config.LOGTAG, "Error retrieving axolotl identity key at" + this.getJid() + ": " + e.getMessage());
+ }
+ }
+ }
+ return identityKeys;
+ }
+ }
+ public boolean addAxolotlIdentityKey(IdentityKey identityKey, boolean trusted) {
+ synchronized (this.keys) {
+ JSONArray keysList;
+ try {
+ keysList = this.keys.getJSONArray("axolotl_identity_key");
+ } catch (JSONException e) {
+ keysList = new JSONArray();
+ }
+ keysList.put(new String(identityKey.serialize()));
+ try {
+ this.keys.put("axolotl_identity_key", keysList);
+ } catch (JSONException e) {
+ Log.e(Config.LOGTAG, "Error adding Identity Key to Contact " + this.getJid() + ": " + e.getMessage());
+ return false;
+ }
}
+ return true;
}
+
public void setOption(int option) {
this.subscription |= 1 << option;
}
@@ -441,24 +493,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;
}
}
diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java
index 957c2a6d..b429354b 100644
--- a/src/main/java/eu/siacs/conversations/entities/Message.java
+++ b/src/main/java/eu/siacs/conversations/entities/Message.java
@@ -65,6 +65,7 @@ public class Message extends AbstractEntity {
protected int encryption;
protected int status;
protected int type;
+ private boolean isTrusted = true;
protected String relativeFilePath;
protected boolean read = true;
protected String remoteMsgId = null;
@@ -663,4 +664,16 @@ public class Message extends AbstractEntity {
public int width = 0;
public int height = 0;
}
+
+ public void trust() {
+ this.isTrusted = true;
+ }
+
+ public void distrust() {
+ this.isTrusted = false;
+ }
+
+ public boolean isTrusted() {
+ return this.isTrusted;
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
index d11b02fa..8d6d63c0 100644
--- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
+++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -1,10 +1,12 @@
package eu.siacs.conversations.persistance;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
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,6 +15,7 @@ import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
+import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
@@ -20,6 +23,11 @@ import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
+import org.whispersystems.libaxolotl.AxolotlAddress;
+import org.whispersystems.libaxolotl.state.PreKeyRecord;
+import org.whispersystems.libaxolotl.state.SessionRecord;
+import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
+
public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
@@ -39,6 +47,40 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ Contact.JID + ") ON CONFLICT REPLACE);";
+ private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+ + AxolotlService.SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+ + AxolotlService.SQLiteAxolotlStore.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.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.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
+ + ") ON CONFLICT REPLACE"
+ +");";
+
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@@ -311,7 +353,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 +523,223 @@ 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(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes());
+ } catch (IOException e) {
+ 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 + " = ? AND ",
+ 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, session.serialize());
+ 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(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes());
+ } 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, record.serialize());
+ 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 = getCursorForPreKey(account, signedPreKeyId);
+ if(cursor.getCount() != 0) {
+ cursor.moveToFirst();
+ try {
+ record = new SignedPreKeyRecord(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes());
+ } 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(cursor.getString(cursor.getColumnIndex(AxolotlService.SQLiteAxolotlStore.KEY)).getBytes()));
+ } catch (IOException ignored) {
+ }
+ }
+ 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, record.serialize());
+ 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);
+ }
}