diff options
author | Daniel Gultsch <daniel@gultsch.de> | 2016-11-17 20:09:42 +0100 |
---|---|---|
committer | Daniel Gultsch <daniel@gultsch.de> | 2016-11-17 20:09:42 +0100 |
commit | 7e2e42cb11fdd8fc126795f6005956ccffe084ff (patch) | |
tree | e2523c145208bcb80c82766a1633d6b015b7c565 | |
parent | 3f3b360eeeabf06c3b7c43ff05ad6ed8cf307473 (diff) |
parse omemo fingerprints from uris
Diffstat (limited to '')
11 files changed, 172 insertions, 44 deletions
diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 211ec0e7..2ee0501a 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/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) { diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java index 15bd9542..b594b5de 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java +++ b/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, diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java index 4a27601f..a3647be7 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java +++ b/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()); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java index 706a5c46..b1d071a4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java @@ -73,7 +73,7 @@ public class XmppAxolotlSession { } protected void setTrust(FingerprintStatus status) { - sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), status); + sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status); } protected FingerprintStatus getTrust() { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 71567356..1e50ce9c 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/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) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 9ebbcf63..dc52e7cf 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/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(); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index ec5559ae..b1609adf 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/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 { diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java index 76081a96..26836395 100644 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ b/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; } } diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java index d8d02c12..c065bf9f 100644 --- a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java +++ b/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(); diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index 15a6c9a1..b2dcc553 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/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) : ""); + } } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index f4d4262d..c12b5e96 100644 --- a/src/main/res/values/strings.xml +++ b/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> |