parse omemo fingerprints from uris

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java     | 27 
src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java  | 14 
src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java | 16 
src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java |  2 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java       | 31 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java    | 25 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java      | 19 
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java              |  5 
src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java              |  5 
src/main/java/eu/siacs/conversations/utils/XmppUri.java                     | 71 
src/main/res/values/strings.xml                                             |  1 
11 files changed, 172 insertions(+), 44 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -98,6 +98,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		return false;
 	}
 
+	public void preVerifyFingerprint(Contact contact, String fingerprint) {
+		axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint);
+	}
+
 	private static class AxolotlAddressMap<T> {
 		protected Map<String, Map<Integer, T>> map;
 		protected final Object MAP_LOCK = new Object();
@@ -200,7 +204,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 		public void put(AxolotlAddress address, XmppAxolotlSession value) {
 			super.put(address, value);
 			value.setNotFresh();
-			xmppConnectionService.syncRosterToDisk(account);
+			xmppConnectionService.syncRosterToDisk(account); //TODO why?
 		}
 
 		public void put(XmppAxolotlSession session) {
@@ -417,7 +421,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public void purgeKey(final String fingerprint) {
-		axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), FingerprintStatus.createCompromised());
+		axolotlStore.setFingerprintStatus(fingerprint.replaceAll("\\s", ""), FingerprintStatus.createCompromised());
 	}
 
 	public void publishOwnDeviceIdIfNeeded() {
@@ -690,7 +694,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 	}
 
 	public void setFingerprintTrust(String fingerprint, FingerprintStatus status) {
-		axolotlStore.setFingerprintTrust(fingerprint, status);
+		axolotlStore.setFingerprintStatus(fingerprint, status);
 	}
 
 	private void verifySessionWithPEP(final XmppAxolotlSession session) {
@@ -749,14 +753,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 
 	private void finishBuildingSessionsFromPEP(final AxolotlAddress address) {
 		AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0);
-		if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
-				&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
+		Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress);
+		Map<Integer, FetchStatus> remote = fetchStatusMap.getAll(address);
+		if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) {
 			FetchStatus report = null;
-			if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED)
-					| fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) {
+			if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) {
+				report = FetchStatus.SUCCESS;
+			} else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) {
 				report = FetchStatus.SUCCESS_VERIFIED;
-			} else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR)
-					|| fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
+			} else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) {
 				report = FetchStatus.ERROR;
 			}
 			mXmppConnectionService.keyStatusUpdated(report);
@@ -812,7 +817,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
 							if (Config.X509_VERIFICATION) {
 								verifySessionWithPEP(session);
 							} else {
-								fetchStatusMap.put(address, FetchStatus.SUCCESS);
+								FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s",""));
+								boolean verified = status != null && status.isVerified();
+								fetchStatusMap.put(address, verified ? FetchStatus.SUCCESS_VERIFIED : FetchStatus.SUCCESS);
 								finishBuildingSessionsFromPEP(address);
 							}
 						} catch (UntrustedIdentityException | InvalidKeyException e) {

src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java 🔗

@@ -114,6 +114,20 @@ public class FingerprintStatus {
         return status;
     }
 
+    public FingerprintStatus toVerified() {
+        FingerprintStatus status = new FingerprintStatus();
+        status.active = active;
+        status.trust = Trust.VERIFIED;
+        return status;
+    }
+
+    public static FingerprintStatus createInactiveVerified() {
+        final FingerprintStatus status = new FingerprintStatus();
+        status.trust = Trust.VERIFIED;
+        status.active = false;
+        return status;
+    }
+
     public enum Trust {
         COMPROMISED,
         UNDECIDED,

src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java 🔗

@@ -187,7 +187,15 @@ public class SQLiteAxolotlStore implements AxolotlStore {
 	@Override
 	public void saveIdentity(String name, IdentityKey identityKey) {
 		if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
-			mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
+			String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
+			FingerprintStatus status = getFingerprintStatus(fingerprint);
+			if (status == null) {
+				status = FingerprintStatus.createActiveUndecided(); //default for new keys
+			} else {
+				status = status.toActive();
+			}
+			mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey, status);
+			trustCache.remove(fingerprint);
 		}
 	}
 
@@ -214,7 +222,7 @@ public class SQLiteAxolotlStore implements AxolotlStore {
 		return (fingerprint == null)? null : trustCache.get(fingerprint);
 	}
 
-	public void setFingerprintTrust(String fingerprint, FingerprintStatus status) {
+	public void setFingerprintStatus(String fingerprint, FingerprintStatus status) {
 		mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status);
 		trustCache.remove(fingerprint);
 	}
@@ -430,4 +438,8 @@ public class SQLiteAxolotlStore implements AxolotlStore {
 	public void removeSignedPreKey(int signedPreKeyId) {
 		mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
 	}
+
+	public void preVerifyFingerprint(Account account, String name, String fingerprint) {
+		mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified());
+	}
 }

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -1106,7 +1106,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 				continue;
 			}
 			try {
-				identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT), 0));
+				String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY));
+				if (key != null) {
+					identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0));
+				} else {
+					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name);
+				}
 			} catch (InvalidKeyException e) {
 				Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
 			}
@@ -1134,10 +1139,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		);
 	}
 
-	private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
-		storeIdentityKey(account, name, own, fingerprint, base64Serialized, FingerprintStatus.createActiveUndecided());
-	}
-
 	private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) {
 		SQLiteDatabase db = this.getWritableDatabase();
 		ContentValues values = new ContentValues();
@@ -1147,6 +1148,22 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
 		values.put(SQLiteAxolotlStore.KEY, base64Serialized);
 		values.putAll(status.toContentValues());
+		String where = SQLiteAxolotlStore.ACCOUNT+"=? AND "+SQLiteAxolotlStore.NAME+"=? AND "+SQLiteAxolotlStore.FINGERPRINT+" =?";
+		String[] whereArgs = {account.getUuid(),name,fingerprint};
+		int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,values,where,whereArgs);
+		if (rows == 0) {
+			db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
+		}
+	}
+
+	public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		ContentValues values = new ContentValues();
+		values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
+		values.put(SQLiteAxolotlStore.NAME, name);
+		values.put(SQLiteAxolotlStore.OWN, 0);
+		values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
+		values.putAll(status.toContentValues());
 		db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
 	}
 
@@ -1227,8 +1244,8 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		}
 	}
 
-	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 storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) {
+		storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status);
 	}
 
 	public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -65,6 +65,7 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.PgpDecryptionService;
 import eu.siacs.conversations.crypto.PgpEngine;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
+import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Blockable;
@@ -102,6 +103,7 @@ import eu.siacs.conversations.utils.PhoneHelper;
 import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
 import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 import eu.siacs.conversations.utils.Xmlns;
+import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xmpp.OnBindListener;
 import eu.siacs.conversations.xmpp.OnContactStatusChanged;
@@ -3608,6 +3610,29 @@ public class XmppConnectionService extends Service {
 		});
 	}
 
+	public void verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
+		boolean needsRosterWrite = false;
+		final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
+		for(XmppUri.Fingerprint fp : fingerprints) {
+			if (fp.type == XmppUri.FingerprintType.OTR) {
+				needsRosterWrite |= contact.addOtrFingerprint(fp.fingerprint);
+			} else if (fp.type == XmppUri.FingerprintType.OMEMO) {
+				String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
+				FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
+				if (fingerprintStatus != null) {
+					if (!fingerprintStatus.isVerified()) {
+						axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
+					}
+				} else {
+					axolotlService.preVerifyFingerprint(contact,fingerprint);
+				}
+			}
+		}
+		if (needsRosterWrite) {
+			syncRosterToDisk(contact.getAccount());
+		}
+	}
+
 	public interface OnMamPreferencesFetched {
 		void onPreferencesFetched(Element prefs);
 		void onPreferencesFetchFailed();

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -397,11 +397,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
     }
 
     @SuppressLint("InflateParams")
-    protected void showCreateContactDialog(final String prefilledJid, final String fingerprint) {
+    protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
         EnterJidDialog dialog = new EnterJidDialog(
                 this, mKnownHosts, mActivatedAccounts,
                 getString(R.string.create_contact), getString(R.string.create),
-                prefilledJid, null, fingerprint == null
+                prefilledJid, null, !invite.hasFingerprints()
         );
 
         dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() {
@@ -420,7 +420,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
                 if (contact.showInRoster()) {
                     throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
                 } else {
-                    contact.addOtrFingerprint(fingerprint);
+                    //contact.addOtrFingerprint(fingerprint);
                     xmppConnectionService.createContact(contact);
                     switchToConversation(contact);
                     return true;
@@ -842,6 +842,10 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
     }
 
     private boolean handleJid(Invite invite) {
+        Log.d(Config.LOGTAG,"handling invite for "+invite.getJid());
+        for(XmppUri.Fingerprint fp : invite.getFingerprints()) {
+            Log.d(Config.LOGTAG,fp.toString());
+        }
         List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid());
         if (invite.isMuc()) {
             Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
@@ -853,16 +857,19 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
                 return false;
             }
         } else if (contacts.size() == 0) {
-            showCreateContactDialog(invite.getJid().toString(), invite.getFingerprint());
+            showCreateContactDialog(invite.getJid().toString(), invite);
             return false;
         } else if (contacts.size() == 1) {
             Contact contact = contacts.get(0);
-            if (invite.getFingerprint() != null) {
+            if (invite.hasFingerprints()) {
+                xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints());
+            }
+            /*if (invite.getFingerprint() != null) {
                 if (contact.addOtrFingerprint(invite.getFingerprint())) {
                     Log.d(Config.LOGTAG, "added new fingerprint");
                     xmppConnectionService.syncRosterToDisk(contact.getAccount());
                 }
-            }
+            }*/
             switchToConversation(contact);
             return true;
         } else {

src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java 🔗

@@ -18,6 +18,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.OmemoActivity;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
@@ -245,7 +246,9 @@ public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdat
 							Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show();
 							break;
 						case SUCCESS_VERIFIED:
-							Toast.makeText(TrustKeysActivity.this,R.string.verified_omemo_key_with_certificate,Toast.LENGTH_LONG).show();
+							Toast.makeText(TrustKeysActivity.this,
+									Config.X509_VERIFICATION ? R.string.verified_omemo_key_with_certificate : R.string.all_omemo_keys_have_been_verified,
+									Toast.LENGTH_LONG).show();
 							break;
 					}
 				}

src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java 🔗

@@ -173,11 +173,10 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer
 
 	protected boolean verifyWithUri(XmppUri uri) {
 		Contact contact = mConversation.getContact();
-		if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) {
-			contact.addOtrFingerprint(uri.getFingerprint());
+		if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) {
+			xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints());
 			Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show();
 			updateView();
-			xmppConnectionService.syncRosterToDisk(contact.getAccount());
 			return true;
 		} else {
 			Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show();

src/main/java/eu/siacs/conversations/utils/XmppUri.java 🔗

@@ -4,7 +4,9 @@ import android.net.Uri;
 
 import java.io.UnsupportedEncodingException;
 import java.net.URLDecoder;
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 
 import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 import eu.siacs.conversations.xmpp.jid.Jid;
@@ -13,7 +15,9 @@ public class XmppUri {
 
 	protected String jid;
 	protected boolean muc;
-	protected String fingerprint;
+	protected List<Fingerprint> fingerprints = new ArrayList<>();
+
+	private static final String OMEMO_URI_PARAM = "omemo-sid-";
 
 	public XmppUri(String uri) {
 		try {
@@ -56,7 +60,7 @@ public class XmppUri {
 			} else {
 				jid = uri.getSchemeSpecificPart().split("\\?")[0];
 			}
-			fingerprint = parseFingerprint(uri.getQuery());
+			this.fingerprints = parseFingerprints(uri.getQuery());
 		} else if ("imto".equalsIgnoreCase(scheme)) {
 			// sample: imto://xmpp/foo@bar.com
 			try {
@@ -73,18 +77,28 @@ public class XmppUri {
 		}
 	}
 
-	protected  String parseFingerprint(String query) {
-		if (query == null) {
-			return null;
-		} else {
-			final String NEEDLE = "otr-fingerprint=";
-			int index = query.indexOf(NEEDLE);
-			if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) {
-				return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40);
-			} else {
-				return null;
+	protected List<Fingerprint> parseFingerprints(String query) {
+		List<Fingerprint> fingerprints = new ArrayList<>();
+		String[] pairs = query == null ? new String[0] : query.split(";");
+		for(String pair : pairs) {
+			String[] parts = pair.split("=",2);
+			if (parts.length == 2) {
+				String key = parts[0].toLowerCase(Locale.US);
+				String value = parts[1];
+				if ("otr-fingerprint".equals(key)) {
+					fingerprints.add(new Fingerprint(FingerprintType.OTR,value));
+				}
+				if (key.startsWith(OMEMO_URI_PARAM)) {
+					try {
+						int id = Integer.parseInt(key.substring(OMEMO_URI_PARAM.length()));
+						fingerprints.add(new Fingerprint(FingerprintType.OMEMO,value,id));
+					} catch (Exception e) {
+						//ignoring invalid device id
+					}
+				}
 			}
 		}
+		return fingerprints;
 	}
 
 	public Jid getJid() {
@@ -95,7 +109,36 @@ public class XmppUri {
 		}
 	}
 
-	public String getFingerprint() {
-		return this.fingerprint;
+	public List<Fingerprint> getFingerprints() {
+		return this.fingerprints;
+	}
+
+	public boolean hasFingerprints() {
+		return fingerprints.size() > 0;
+	}
+	public enum FingerprintType {
+		OMEMO,
+		OTR
+	}
+
+	public static class Fingerprint {
+		public final FingerprintType type;
+		public final String fingerprint;
+		public final int deviceId;
+
+		public Fingerprint(FingerprintType type, String fingerprint) {
+			this(type, fingerprint, 0);
+		}
+
+		public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
+			this.type = type;
+			this.fingerprint = fingerprint;
+			this.deviceId = deviceId;
+		}
+
+		@Override
+		public String toString() {
+			return type.toString()+": "+fingerprint+(deviceId != 0 ? " "+String.valueOf(deviceId) : "");
+		}
 	}
 }

src/main/res/values/strings.xml 🔗

@@ -702,4 +702,5 @@
 	<string name="error_unable_to_create_temporary_file">Unable to create temporary file</string>
 	<string name="this_device_has_been_verified">This device has been verified</string>
 	<string name="copy_fingerprint">Copy fingerprint</string>
+	<string name="all_omemo_keys_have_been_verified">All OMEMO keys have been verified</string>
 </resources>