diff options
Diffstat (limited to 'src')
17 files changed, 362 insertions, 71 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 e22b05c00..31b23e3cb 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -146,8 +146,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { for (Integer deviceId : deviceIds) { AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); - String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", ""); - this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint)); + IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); + this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); } } @@ -575,12 +575,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { return axolotlStore.getFingerprintTrust(fingerprint); } + public X509Certificate getFingerprintCertificate(String fingerprint) { + return axolotlStore.getFingerprintCertificate(fingerprint); + } + public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) { axolotlStore.setFingerprintTrust(fingerprint, trust); } - private void verifySessionWithPEP(final XmppAxolotlSession session, final IdentityKey identityKey) { + private void verifySessionWithPEP(final XmppAxolotlSession session) { + Log.d(Config.LOGTAG, "trying to verify fresh session (" + session.getRemoteAddress().getName() + ") with pep"); final AxolotlAddress address = session.getRemoteAddress(); + final IdentityKey identityKey = session.getIdentityKey(); try { IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveVerificationForDevice(Jid.fromString(address.getName()), address.getDeviceId()); mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { @@ -595,8 +601,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { if (verifier.verify(verification.second)) { try { mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA"); - Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+session.getFingerprint()); - setFingerprintTrust(session.getFingerprint(), XmppAxolotlSession.Trust.TRUSTED_X509); + String fingerprint = session.getFingerprint(); + Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint); + setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509); + axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); finishBuildingSessionsFromPEP(address); return; @@ -607,6 +615,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } catch (Exception e) { Log.d(Config.LOGTAG, "error during verification " + e.getMessage()); } + } else { + Log.d(Config.LOGTAG,"no verification found"); } fetchStatusMap.put(address, FetchStatus.SUCCESS); finishBuildingSessionsFromPEP(address); @@ -678,10 +688,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { try { SessionBuilder builder = new SessionBuilder(axolotlStore, address); builder.process(preKeyBundle); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", "")); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey()); sessions.put(address, session); if (Config.X509_VERIFICATION) { - verifySessionWithPEP(session, bundle.getIdentityKey()); + verifySessionWithPEP(session); } else { fetchStatusMap.put(address, FetchStatus.SUCCESS); finishBuildingSessionsFromPEP(address); @@ -718,7 +728,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); if (identityKey != null) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); sessions.put(address, session); } else { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId); @@ -740,7 +750,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); if (identityKey != null) { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache..."); - XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", "")); + XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey); sessions.put(address, session); } else { Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId); @@ -889,8 +899,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) { IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey(); return (identityKey != null) - ? new XmppAxolotlSession(account, axolotlStore, address, - identityKey.getFingerprint().replaceAll("\\s", "")) + ? new XmppAxolotlSession(account, axolotlStore, address, identityKey) : null; } @@ -944,11 +953,11 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { } private void putFreshSession(XmppAxolotlSession session) { + Log.d(Config.LOGTAG,"put fresh session"); sessions.put(session); if (Config.X509_VERIFICATION) { - IdentityKey identityKey = axolotlStore.loadSession(session.getRemoteAddress()).getSessionState().getRemoteIdentityKey(); - if (identityKey != null) { - verifySessionWithPEP(session, identityKey); + if (session.getIdentityKey() != null) { + verifySessionWithPEP(session); } else { Log.e(Config.LOGTAG,account.getJid().toBareJid()+": identity key was empty after reloading for x509 verification"); } 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 a78317183..3c8cb3c10 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/SQLiteAxolotlStore.java @@ -15,6 +15,7 @@ import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; import org.whispersystems.libaxolotl.util.KeyHelper; +import java.security.cert.X509Certificate; import java.util.List; import java.util.Set; @@ -36,6 +37,7 @@ public class SQLiteAxolotlStore implements AxolotlStore { public static final String NAME = "name"; public static final String TRUSTED = "trusted"; public static final String OWN = "ownkey"; + public static final String CERTIFICATE = "certificate"; public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id"; public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id"; @@ -213,6 +215,14 @@ public class SQLiteAxolotlStore implements AxolotlStore { trustCache.remove(fingerprint); } + public void setFingerprintCertificate(String fingerprint, X509Certificate x509Certificate) { + mXmppConnectionService.databaseBackend.setIdentityKeyCertificate(account, fingerprint, x509Certificate); + } + + public X509Certificate getFingerprintCertificate(String fingerprint) { + return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint); + } + public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) { return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust); } 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 c452acfd4..b713eb5fe 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java @@ -6,6 +6,7 @@ import android.util.Log; import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.DuplicateMessageException; +import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidKeyIdException; import org.whispersystems.libaxolotl.InvalidMessageException; @@ -29,7 +30,7 @@ public class XmppAxolotlSession { private final SQLiteAxolotlStore sqLiteAxolotlStore; private final AxolotlAddress remoteAddress; private final Account account; - private String fingerprint = null; + private IdentityKey identityKey; private Integer preKeyId = null; private boolean fresh = true; @@ -103,9 +104,9 @@ public class XmppAxolotlSession { } } - public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) { + public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, IdentityKey identityKey) { this(account, store, remoteAddress); - this.fingerprint = fingerprint.replaceAll("\\s",""); + this.identityKey = identityKey; } public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) { @@ -125,7 +126,11 @@ public class XmppAxolotlSession { } public String getFingerprint() { - return fingerprint; + return identityKey == null ? null : identityKey.getFingerprint().replaceAll("\\s", ""); + } + + public IdentityKey getIdentityKey() { + return identityKey; } public AxolotlAddress getRemoteAddress() { @@ -141,11 +146,11 @@ public class XmppAxolotlSession { } protected void setTrust(Trust trust) { - sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust); + sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), trust); } protected Trust getTrust() { - Trust trust = sqLiteAxolotlStore.getFingerprintTrust(fingerprint); + Trust trust = sqLiteAxolotlStore.getFingerprintTrust(getFingerprint()); return (trust == null) ? Trust.UNDECIDED : trust; } @@ -164,11 +169,11 @@ public class XmppAxolotlSession { try { PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey); Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); - String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", ""); - if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.fingerprint + ", received message with fingerprint " + fingerprint); + IdentityKey msgIdentityKey = message.getIdentityKey(); + if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) { + Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint()); } else { - this.fingerprint = fingerprint; + this.identityKey = msgIdentityKey; plaintext = cipher.decrypt(message); if (message.getPreKeyId().isPresent()) { preKeyId = message.getPreKeyId().get(); diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index f5fa88d7a..9c8beb1ec 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -11,6 +11,7 @@ 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; @@ -104,7 +105,9 @@ public class Contact implements ListItem, Blockable { } public String getDisplayName() { - if (this.systemName != null) { + if (this.presenceName != null && Config.X509_VERIFICATION) { + return this.presenceName; + } else if (this.systemName != null) { return this.systemName; } else if (this.serverName != null) { return this.serverName; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 81f789f8d..bc7f302ae 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -21,6 +21,7 @@ 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.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -621,8 +622,16 @@ public class Conversation extends AbstractEntity implements Blockable { } public int getNextEncryption() { + final AxolotlService axolotlService = getAccount().getAxolotlService(); int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); if (next == -1) { + if (Config.X509_VERIFICATION && mode == MODE_SINGLE) { + if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) { + return Message.ENCRYPTION_AXOLOTL; + } else { + return Message.ENCRYPTION_NONE; + } + } int outgoing = this.getMostRecentlyUsedOutgoingEncryption(); if (outgoing == Message.ENCRYPTION_NONE) { next = this.getMostRecentlyUsedIncomingEncryption(); @@ -631,7 +640,7 @@ public class Conversation extends AbstractEntity implements Blockable { } } if (Config.FORCE_ENCRYPTION && mode == MODE_SINGLE && next <= 0) { - if (getAccount().getAxolotlService().isContactAxolotlCapable(getContact())) { + if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) { return Message.ENCRYPTION_AXOLOTL; } else { return Message.ENCRYPTION_OTR; diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index e482f0f85..3077c4889 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -19,7 +19,12 @@ import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; @@ -44,7 +49,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 21; + private static final int DATABASE_VERSION = 22; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -102,6 +107,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + SQLiteAxolotlStore.NAME + " TEXT, " + SQLiteAxolotlStore.OWN + " INTEGER, " + SQLiteAxolotlStore.FINGERPRINT + " TEXT, " + + SQLiteAxolotlStore.CERTIFICATE + " BLOB, " + SQLiteAxolotlStore.TRUSTED + " INTEGER, " + SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY(" + SQLiteAxolotlStore.ACCOUNT @@ -345,6 +351,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { + "=?", new String[]{account.getUuid()}); } } + + if (oldVersion < 22 && newVersion >= 22) { + db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -592,16 +602,16 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args); } - public Pair<Long,String> getLastMessageReceived(Account account) { + public Pair<Long, String> getLastMessageReceived(Account account) { SQLiteDatabase db = this.getReadableDatabase(); String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1"; String[] args = {account.getUuid()}; - Cursor cursor = db.rawQuery(sql,args); - if (cursor.getCount() ==0) { + Cursor cursor = db.rawQuery(sql, args); + if (cursor.getCount() == 0) { return null; } else { cursor.moveToFirst(); - return new Pair<>(cursor.getLong(0),cursor.getString(1)); + return new Pair<>(cursor.getLong(0), cursor.getString(1)); } } @@ -1050,6 +1060,52 @@ public class DatabaseBackend extends SQLiteOpenHelper { return rows == 1; } + public boolean setIdentityKeyCertificate(Account account, String fingerprint, X509Certificate x509Certificate) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] selectionArgs = { + account.getUuid(), + fingerprint + }; + try { + ContentValues values = new ContentValues(); + values.put(SQLiteAxolotlStore.CERTIFICATE, x509Certificate.getEncoded()); + return db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values, + SQLiteAxolotlStore.ACCOUNT + " = ? AND " + + SQLiteAxolotlStore.FINGERPRINT + " = ? ", + selectionArgs) == 1; + } catch (CertificateEncodingException e) { + Log.d(Config.LOGTAG, "could not encode certificate"); + return false; + } + } + + public X509Certificate getIdentityKeyCertifcate(Account account, String fingerprint) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = { + account.getUuid(), + fingerprint + }; + String[] colums = {SQLiteAxolotlStore.CERTIFICATE}; + String selection = SQLiteAxolotlStore.ACCOUNT + " = ? AND " + SQLiteAxolotlStore.FINGERPRINT + " = ? "; + Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME, colums, selection, selectionArgs, null, null, null); + if (cursor.getCount() < 1) { + return null; + } else { + cursor.moveToFirst(); + byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE)); + if (certificate == null || certificate.length == 0) { + return null; + } + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) certificateFactory.generateCertificate(new ByteArrayInputStream(certificate)); + } catch (CertificateException e) { + Log.d(Config.LOGTAG,"certificate exception "+e.getMessage()); + return null; + } + } + } + public void storeIdentityKey(Account account, String name, IdentityKey identityKey) { storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT)); } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 649ddacd8..28d6e3550 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -50,6 +50,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.List; @@ -992,13 +993,16 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void onIqPacketReceived(final Account account, final IqPacket packet) { if (packet.getType() == IqPacket.TYPE.RESULT) { final Element query = packet.query(); - final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); + final HashMap<Jid, Bookmark> bookmarks = new HashMap<>(); final Element storage = query.findChild("storage", "storage:bookmarks"); if (storage != null) { for (final Element item : storage.getChildren()) { if (item.getName().equals("conference")) { final Bookmark bookmark = Bookmark.parse(item, account); - bookmarks.add(bookmark); + Bookmark old = bookmarks.put(bookmark.getJid(), bookmark); + if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) { + bookmark.setBookmarkName(old.getBookmarkName()); + } Conversation conversation = find(bookmark); if (conversation != null) { conversation.setBookmark(bookmark); @@ -1011,7 +1015,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } } - account.setBookmarks(bookmarks); + account.setBookmarks(new ArrayList<>(bookmarks.values())); } else { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks"); } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index aff50502c..70cd982b1 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -14,6 +14,7 @@ import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; @@ -27,15 +28,18 @@ import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.QuickContactBadge; import android.widget.TextView; +import android.widget.Toast; import org.openintents.openpgp.util.OpenPgpUtils; +import java.security.cert.X509Certificate; import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; @@ -394,7 +398,12 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } for (final String fingerprint : contact.getAccount().getAxolotlService().getFingerprintsForContact(contact)) { boolean highlight = fingerprint.equals(messageFingerprint); - hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight); + hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight, new OnClickListener() { + @Override + public void onClick(View v) { + onOmemoKeyClicked(contact.getAccount(), fingerprint); + } + }); } if (contact.getPgpKeyId() != 0) { hasKeys = true; @@ -446,6 +455,40 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } } + private void onOmemoKeyClicked(Account account, String fingerprint) { + final XmppAxolotlSession.Trust trust = account.getAxolotlService().getFingerprintTrust(fingerprint); + if (trust != null && trust == XmppAxolotlSession.Trust.TRUSTED_X509) { + X509Certificate x509Certificate = account.getAxolotlService().getFingerprintCertificate(fingerprint); + if (x509Certificate != null) { + showCertificateInformationDialog(CryptoHelper.extractCertificateInformation(x509Certificate)); + } else { + Toast.makeText(this,R.string.certificate_not_found, Toast.LENGTH_SHORT).show(); + } + } + } + + private void showCertificateInformationDialog(Bundle bundle) { + View view = getLayoutInflater().inflate(R.layout.certificate_information, null); + final String not_available = getString(R.string.certicate_info_not_available); + TextView subject_cn = (TextView) view.findViewById(R.id.subject_cn); + TextView subject_o = (TextView) view.findViewById(R.id.subject_o); + TextView issuer_cn = (TextView) view.findViewById(R.id.issuer_cn); + TextView issuer_o = (TextView) view.findViewById(R.id.issuer_o); + TextView sha1 = (TextView) view.findViewById(R.id.sha1); + + subject_cn.setText(bundle.getString("subject_cn", not_available)); + subject_o.setText(bundle.getString("subject_o", not_available)); + issuer_cn.setText(bundle.getString("issuer_cn", not_available)); + issuer_o.setText(bundle.getString("issuer_o", not_available)); + sha1.setText(bundle.getString("sha1", not_available)); + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.certificate_information); + builder.setView(view); + builder.setPositiveButton(R.string.ok, null); + builder.create().show(); + } + protected void confirmToDeleteFingerprint(final String fingerprint) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.delete_fingerprint); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 47edd1a42..706a2cbd3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -468,7 +468,7 @@ public class ConversationActivity extends XmppActivity menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating()); menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite()); menuSecure.setVisible(!Config.HIDE_ENCRYPTION_IN_UI); - menuSecure.setVisible(!Config.HIDE_PGP_IN_UI); //if pgp is hidden conferences have no choice of encryption + menuSecure.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); //if pgp is hidden conferences have no choice of encryption } else { menuMucDetails.setVisible(false); } @@ -922,8 +922,9 @@ public class ConversationActivity extends XmppActivity MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none); MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp); MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl); - pgp.setVisible(!Config.HIDE_PGP_IN_UI); + pgp.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); none.setVisible(!Config.FORCE_ENCRYPTION); + otr.setVisible(!Config.X509_VERIFICATION); if (conversation.getMode() == Conversation.MODE_MULTI) { otr.setVisible(false); axolotl.setVisible(false); diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 439ada3a6..f5356d24f 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -710,7 +710,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate continue; } boolean highlight = fingerprint.equals(messageFingerprint); - hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight); + hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight, null); } if (hasKeys) { keysCard.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java index 4bd0d42da..2e6d32466 100644 --- a/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java @@ -126,6 +126,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate // own fingerprints have no impact on locked status. } }, + null, null ); } @@ -140,6 +141,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate lockOrUnlockAsNeeded(); } }, + null, null ); } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 4ca18cf8e..d05f50817 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -651,7 +651,7 @@ public abstract class XmppActivity extends Activity { builder.create().show(); } - protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight) { + protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) { final XmppAxolotlSession.Trust trust = account.getAxolotlService() .getFingerprintTrust(fingerprint); if (trust == null) { @@ -673,7 +673,8 @@ public abstract class XmppActivity extends Activity { XmppAxolotlSession.Trust.UNTRUSTED); v.setEnabled(true); } - } + }, + onKeyClickedListener ); } @@ -685,24 +686,30 @@ public abstract class XmppActivity extends Activity { boolean showTag, CompoundButton.OnCheckedChangeListener onCheckedChangeListener, - View.OnClickListener onClickListener) { + View.OnClickListener onClickListener, + View.OnClickListener onKeyClickedListener) { if (trust == XmppAxolotlSession.Trust.COMPROMISED) { return false; } View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); + key.setOnClickListener(onKeyClickedListener); TextView keyType = (TextView) view.findViewById(R.id.key_type); + keyType.setOnClickListener(onKeyClickedListener); Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust); trustToggle.setVisibility(View.VISIBLE); trustToggle.setOnCheckedChangeListener(onCheckedChangeListener); trustToggle.setOnClickListener(onClickListener); - view.setOnLongClickListener(new View.OnLongClickListener() { + final View.OnLongClickListener purge = new View.OnLongClickListener() { @Override public boolean onLongClick(View v) { showPurgeKeyDialog(account, fingerprint); return true; } - }); + }; + view.setOnLongClickListener(purge); + key.setOnLongClickListener(purge); + keyType.setOnLongClickListener(purge); boolean x509 = trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509; switch (trust) { case UNTRUSTED: @@ -752,7 +759,7 @@ public abstract class XmppActivity extends Activity { keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint)); } - key.setText(CryptoHelper.prettifyFingerprint(fingerprint)); + key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2))); keys.addView(view); return true; } @@ -762,7 +769,7 @@ public abstract class XmppActivity extends Activity { builder.setTitle(getString(R.string.purge_key)); builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.setMessage(getString(R.string.purge_key_desc_part1) - + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint) + + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2)) + "\n\n" + getString(R.string.purge_key_desc_part2)); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton(getString(R.string.accept), diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index a3cea867f..a54407dec 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -18,6 +18,7 @@ import android.text.style.ForegroundColorSpan; import android.text.style.RelativeSizeSpan; import android.text.style.StyleSpan; import android.util.DisplayMetrics; +import android.util.Patterns; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -32,11 +33,11 @@ import android.widget.Toast; import java.lang.ref.WeakReference; import java.util.List; import java.util.concurrent.RejectedExecutionException; +import java.util.regex.Matcher; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; @@ -246,6 +247,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setText(text); viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false)); viewHolder.messageBody.setTypeface(null, Typeface.ITALIC); + viewHolder.messageBody.setTextIsSelectable(false); } private void displayDecryptionFailed(ViewHolder viewHolder, boolean darkBackground) { @@ -258,6 +260,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { R.string.decryption_failed)); viewHolder.messageBody.setTextColor(getMessageTextColor(darkBackground, false)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setTextIsSelectable(false); } private void displayHeartMessage(final ViewHolder viewHolder, final String body) { @@ -332,13 +335,21 @@ public class MessageAdapter extends ArrayAdapter<Message> { } viewHolder.messageBody.setText(span); } + int urlCount = 0; + Matcher matcher = Patterns.WEB_URL.matcher(body); + while (matcher.find()) { + urlCount++; + } + viewHolder.messageBody.setTextIsSelectable(urlCount <= 1); } else { viewHolder.messageBody.setText(""); + viewHolder.messageBody.setTextIsSelectable(false); } viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true)); viewHolder.messageBody.setLinkTextColor(this.getMessageTextColor(darkBackground, true)); viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground ? R.color.grey800 : R.color.grey500)); viewHolder.messageBody.setTypeface(null, Typeface.NORMAL); + viewHolder.messageBody.setOnLongClickListener(openContextMenu); } private void displayDownloadableMessage(ViewHolder viewHolder, diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index ab407249c..1ef5fb3f6 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -1,19 +1,17 @@ package eu.siacs.conversations.utils; -import android.util.Log; +import android.os.Bundle; import android.util.Pair; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x500.style.BCStyle; import org.bouncycastle.asn1.x500.style.IETFUtils; import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; -import org.bouncycastle.jce.PrincipalUtil; -import java.security.SecureRandom; +import java.security.MessageDigest; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; -import java.security.cert.X509Extension; import java.text.Normalizer; import java.util.ArrayList; import java.util.Arrays; @@ -21,6 +19,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -31,8 +30,6 @@ import eu.siacs.conversations.xmpp.jid.Jid; public final class CryptoHelper { public static final String FILETRANSFER = "?FILETRANSFERv1:"; private final static char[] hexArray = "0123456789abcdef".toCharArray(); - private final static char[] vowels = "aeiou".toCharArray(); - private final static char[] consonants = "bcdfghjklmnpqrstvwxyz".toCharArray(); final public static byte[] ONE = new byte[] { 0, 0, 0, 1 }; public static String bytesToHex(byte[] bytes) { @@ -66,22 +63,6 @@ public final class CryptoHelper { return result; } - public static String randomMucName(SecureRandom random) { - return randomWord(3, random) + "." + randomWord(7, random); - } - - private static String randomWord(int lenght, SecureRandom random) { - StringBuilder builder = new StringBuilder(lenght); - for (int i = 0; i < lenght; ++i) { - if (i % 2 == 0) { - builder.append(consonants[random.nextInt(consonants.length)]); - } else { - builder.append(vowels[random.nextInt(vowels.length)]); - } - } - return builder.toString(); - } - /** * Escapes usernames or passwords for SASL. */ @@ -114,13 +95,21 @@ public final class CryptoHelper { } else if (fingerprint.length() < 40) { return fingerprint; } - StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s","")); + StringBuilder builder = new StringBuilder(fingerprint.toLowerCase(Locale.US).replaceAll("\\s", "")); for(int i=8;i<builder.length();i+=9) { builder.insert(i, ' '); } return builder.toString(); } + public static String prettifyFingerprintCert(String fingerprint) { + StringBuilder builder = new StringBuilder(fingerprint); + for(int i=2;i < builder.length(); i+=3) { + builder.insert(i,':'); + } + return builder.toString(); + } + public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) { final Collection<String> cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS)); final List<String> platformCiphers = Arrays.asList(platformSupportedCipherSuites); @@ -167,6 +156,46 @@ public final class CryptoHelper { } } + public static Bundle extractCertificateInformation(X509Certificate certificate) { + Bundle information = new Bundle(); + try { + JcaX509CertificateHolder holder = new JcaX509CertificateHolder(certificate); + X500Name subject = holder.getSubject(); + try { + information.putString("subject_cn", subject.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString()); + } catch (Exception e) { + //ignored + } + try { + information.putString("subject_o",subject.getRDNs(BCStyle.O)[0].getFirst().getValue().toString()); + } catch (Exception e) { + //ignored + } + + X500Name issuer = holder.getIssuer(); + try { + information.putString("issuer_cn", issuer.getRDNs(BCStyle.CN)[0].getFirst().getValue().toString()); + } catch (Exception e) { + //ignored + } + try { + information.putString("issuer_o", issuer.getRDNs(BCStyle.O)[0].getFirst().getValue().toString()); + } catch (Exception e) { + //ignored + } + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + byte[] fingerprint = md.digest(certificate.getEncoded()); + information.putString("sha1", prettifyFingerprintCert(bytesToHex(fingerprint))); + } catch (Exception e) { + + } + return information; + } catch (CertificateEncodingException e) { + return information; + } + } + public static int encryptionTypeToText(int encryption) { switch (encryption) { case Message.ENCRYPTION_OTR: diff --git a/src/main/res/layout/certificate_information.xml b/src/main/res/layout/certificate_information.xml new file mode 100644 index 000000000..4c085459b --- /dev/null +++ b/src/main/res/layout/certificate_information.xml @@ -0,0 +1,88 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="16dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/certificate_subject" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeHeadline"/> + <TextView + android:layout_marginTop="8dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/certificate_cn" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/subject_cn" + android:textColor="@color/black54" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_marginTop="8dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/certificate_o" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/subject_o" + android:textColor="@color/black54" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_marginTop="16dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/certificate_issuer" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeHeadline"/> + <TextView + android:layout_marginTop="8dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/certificate_cn" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/issuer_cn" + android:textColor="@color/black54" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_marginTop="8dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/certificate_o" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/issuer_o" + android:textColor="@color/black54" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_marginTop="16dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/certificate_sha1" + android:textColor="@color/black87" + android:textSize="?attr/TextSizeBody"/> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/sha1" + android:textColor="@color/black54" + android:textSize="?attr/TextSizeBody" + android:typeface="monospace" + android:fontFamily="monospace"/> +</LinearLayout>
\ No newline at end of file diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 507618980..35b009edf 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -88,13 +88,14 @@ <string name="openkeychain_required_long">Pix-Art Messenger benutzt eine Drittanwendung namens <b>OpenKeychain</b>, um Nachrichten zu ver- und entschlüsseln und um deine Schlüssel zu verwalten.\n\nOpenKeychain ist GPLv3-lizenziert und kann über F-Droid oder Google Play bezogen werden.\n\n<small>(Bitte starte Pix-Art Messenger danach neu.)</small></string> <string name="restart">Neu starten</string> <string name="install">Installieren</string> + <string name="openkeychain_not_installed">Bitte OpenKeychain installieren</string> <string name="offering">angeboten…</string> <string name="waiting">warten…</string> <string name="no_pgp_key">Kein OpenPGP-Schlüssel gefunden</string> <string name="contact_has_no_pgp_key">Pix-Art Messenger ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge OpenPGP einrichten.</small></string> <string name="no_pgp_keys">Keine OpenPGP-Schlüssel gefunden</string> <string name="contacts_have_no_pgp_keys">Pix-Art Messenger ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil deine Kontakte ihre Schlüssel nicht preisgeben.\n\n<small>Bitte sage deinen Kontakten, sie mögen OpenPGP einrichten.</small></string> - <string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier, um sie zu entschlüsseln und anzuzeigen.</i></string> + <string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Antippen zum Entschlüsseln.</i></string> <string name="pref_general">Allgemeines</string> <string name="pref_xmpp_resource">XMPP-Ressource</string> <string name="pref_xmpp_resource_summary">Der Name, mit dem sich der Client selbst identifiziert</string> @@ -213,7 +214,7 @@ <string name="this_device_omemo_fingerprint">Eigener OMEMO-Fingerabdruck</string> <string name="other_devices">Andere Geräte</string> <string name="trust_omemo_fingerprints">OMEMO-Fingerabdruck vertrauen</string> - <string name="fetching_keys">Schlüssel empfangen…</string> + <string name="fetching_keys">Schlüssel werden abgerufen …</string> <string name="done">Erledigt</string> <string name="verify">Überprüfen</string> <string name="decrypt">Entschlüsseln</string> @@ -551,4 +552,9 @@ <item quantity="one">%d Nachricht</item> <item quantity="other">%d Nachrichten</item> </plurals> + <string name="shared_file_with_x">Datei mit %s geteilt</string> + <string name="shared_image_with_x">Bild mit %s geteilt</string> + <string name="no_storage_permission">Conversations benötigt Zugriff auf externen Speicher</string> + <string name="sync_with_contacts">Mit Kontakten synchronisieren</string> + <string name="sync_with_contacts_long">Conversations möchte deine XMPP-Kontaktliste mit deinen Kontakten abgleichen, um deren vollständige Namen und Avatare anzuzeigen.\n\nConversations wird deine Kontakte nur lokal lesen und abgleichen und überträgt diese nicht auf den Server.\n\nDu wirst nun gefragt, ob du den Zugriff auf deine Kontakte erlauben möchtest.</string> </resources> diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index e2d4e6a0c..15dd5f7f0 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -580,4 +580,12 @@ <string name="no_storage_permission">Pix-Art Messenger needs access to external storage</string> <string name="sync_with_contacts">Synchronize with contacts</string> <string name="sync_with_contacts_long">Pix-Art Messenger wants to match your XMPP roster with your contacts to show their full names and avatars.\n\nPix-Art Messenger will only read your contacts and match them locally without uploading them to your server.\n\nYou will now be asked to grant permission to access your contacts.</string> + <string name="certificate_information">Certificate Information</string> + <string name="certificate_subject">Subject</string> + <string name="certificate_issuer">Issuer</string> + <string name="certificate_cn">Common Name</string> + <string name="certificate_o">Organization</string> + <string name="certificate_sha1">SHA1</string> + <string name="certicate_info_not_available">(Not available)</string> + <string name="certificate_not_found">No certificate found</string> </resources> |