diff --git a/build.gradle b/build.gradle index d998f00a..2149fc7d 100644 --- a/build.gradle +++ b/build.gradle @@ -81,8 +81,8 @@ android { defaultConfig { minSdkVersion 14 targetSdkVersion 24 - versionCode 1480 - versionName "2.09" + versionCode 1481 + versionName "2.10" archivesBaseName += "-$versionName" applicationId "net.atomarea.flowx" multiDexEnabled true diff --git a/src/main/java/net/atomarea/flowx/Config.java b/src/main/java/net/atomarea/flowx/Config.java index de84e9ae..d0cb652c 100644 --- a/src/main/java/net/atomarea/flowx/Config.java +++ b/src/main/java/net/atomarea/flowx/Config.java @@ -7,24 +7,14 @@ public final class Config { private static final int UNENCRYPTED = 1; - private static final int OPENPGP = 2; - private static final int OTR = 4; private static final int OMEMO = 8; - private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO; + private static final int ENCRYPTION_MASK = UNENCRYPTED | OMEMO; public static boolean supportUnencrypted() { return (ENCRYPTION_MASK & UNENCRYPTED) != 0; } - public static boolean supportOpenPgp() { - return (ENCRYPTION_MASK & OPENPGP) != 0; - } - - public static boolean supportOtr() { - return (ENCRYPTION_MASK & OTR) != 0; - } - public static boolean supportOmemo() { return (ENCRYPTION_MASK & OMEMO) != 0; } diff --git a/src/main/java/net/atomarea/flowx/crypto/axolotl/AxolotlService.java b/src/main/java/net/atomarea/flowx/crypto/axolotl/AxolotlService.java index 80d67ba7..2f728e83 100644 --- a/src/main/java/net/atomarea/flowx/crypto/axolotl/AxolotlService.java +++ b/src/main/java/net/atomarea/flowx/crypto/axolotl/AxolotlService.java @@ -112,6 +112,15 @@ public void preVerifyFingerprint(Account account, String fingerprint) { axolotlStore.preVerifyFingerprint(account, account.getJid().toBareJid().toPreppedString(), fingerprint); } + public boolean hasVerifiedKeys(String name) { + for(XmppAxolotlSession session : this.sessions.getAll(new AxolotlAddress(name,0)).values()) { + if (session.getTrust().isVerified()) { + return true; + } + } + return false; + } + private static class AxolotlAddressMap { protected Map> map; protected final Object MAP_LOCK = new Object(); @@ -226,6 +235,7 @@ public enum FetchStatus { SUCCESS, SUCCESS_VERIFIED, TIMEOUT, + SUCCESS_TRUSTED, ERROR } @@ -405,6 +415,7 @@ public void registerDevices(final Jid jid, @NonNull final Set deviceIds } } this.deviceIds.put(jid, deviceIds); + mXmppConnectionService.updateConversationUi(); //update the lock icon mXmppConnectionService.keyStatusUpdated(null); } @@ -779,6 +790,8 @@ private void finishBuildingSessionsFromPEP(final AxolotlAddress address) { report = FetchStatus.SUCCESS; } else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) { report = FetchStatus.SUCCESS_VERIFIED; + } else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) { + report = FetchStatus.SUCCESS_TRUSTED; } else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) { report = FetchStatus.ERROR; } @@ -836,8 +849,15 @@ public void onIqPacketReceived(Account account, IqPacket packet) { verifySessionWithPEP(session); } else { FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s","")); - boolean verified = status != null && status.isVerified(); - fetchStatusMap.put(address, verified ? FetchStatus.SUCCESS_VERIFIED : FetchStatus.SUCCESS); + FetchStatus fetchStatus; + if (status != null && status.isVerified()) { + fetchStatus = FetchStatus.SUCCESS_VERIFIED; + } else if (status != null && status.isTrusted()) { + fetchStatus = FetchStatus.SUCCESS_TRUSTED; + } else { + fetchStatus = FetchStatus.SUCCESS; + } + fetchStatusMap.put(address, fetchStatus); finishBuildingSessionsFromPEP(address); } } catch (UntrustedIdentityException | InvalidKeyException e) { diff --git a/src/main/java/net/atomarea/flowx/crypto/axolotl/FingerprintStatus.java b/src/main/java/net/atomarea/flowx/crypto/axolotl/FingerprintStatus.java index 2d5ad9c3..80d40fa2 100644 --- a/src/main/java/net/atomarea/flowx/crypto/axolotl/FingerprintStatus.java +++ b/src/main/java/net/atomarea/flowx/crypto/axolotl/FingerprintStatus.java @@ -63,6 +63,14 @@ public static FingerprintStatus createActiveUndecided() { return status; } + public static FingerprintStatus createActiveTrusted() { + final FingerprintStatus status = new FingerprintStatus(); + status.trust = Trust.TRUSTED; + status.active = true; + status.lastActivation = System.currentTimeMillis(); + return status; + } + public static FingerprintStatus createActiveVerified(boolean x509) { final FingerprintStatus status = new FingerprintStatus(); status.trust = x509 ? Trust.VERIFIED_X509 : Trust.VERIFIED; diff --git a/src/main/java/net/atomarea/flowx/crypto/axolotl/SQLiteAxolotlStore.java b/src/main/java/net/atomarea/flowx/crypto/axolotl/SQLiteAxolotlStore.java index b3e73db2..1e4605c3 100644 --- a/src/main/java/net/atomarea/flowx/crypto/axolotl/SQLiteAxolotlStore.java +++ b/src/main/java/net/atomarea/flowx/crypto/axolotl/SQLiteAxolotlStore.java @@ -21,7 +21,10 @@ import net.atomarea.flowx.Config; import net.atomarea.flowx.entities.Account; +import net.atomarea.flowx.entities.Contact; import net.atomarea.flowx.services.XmppConnectionService; +import net.atomarea.flowx.xmpp.jid.InvalidJidException; +import net.atomarea.flowx.xmpp.jid.Jid; public class SQLiteAxolotlStore implements AxolotlStore { @@ -191,7 +194,12 @@ public void saveIdentity(String name, IdentityKey identityKey) { String fingerprint = identityKey.getFingerprint().replaceAll("\\s", ""); FingerprintStatus status = getFingerprintStatus(fingerprint); if (status == null) { - status = FingerprintStatus.createActiveUndecided(); //default for new keys + if (mXmppConnectionService.blindTrustBeforeVerification() && !account.getAxolotlService().hasVerifiedKeys(name)) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": blindly trusted "+fingerprint+" of "+name); + status = FingerprintStatus.createActiveTrusted(); + } else { + status = FingerprintStatus.createActiveUndecided(); + } } else { status = status.toActive(); } diff --git a/src/main/java/net/atomarea/flowx/entities/Conversation.java b/src/main/java/net/atomarea/flowx/entities/Conversation.java index 9b865ab1..733f2744 100644 --- a/src/main/java/net/atomarea/flowx/entities/Conversation.java +++ b/src/main/java/net/atomarea/flowx/entities/Conversation.java @@ -38,7 +38,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl public static final String MODE = "mode"; public static final String ATTRIBUTES = "attributes"; - public static final String ATTRIBUTE_NEXT_ENCRYPTION = ""; + public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; diff --git a/src/main/java/net/atomarea/flowx/entities/Message.java b/src/main/java/net/atomarea/flowx/entities/Message.java index 8e961b1b..664f555e 100644 --- a/src/main/java/net/atomarea/flowx/entities/Message.java +++ b/src/main/java/net/atomarea/flowx/entities/Message.java @@ -495,7 +495,7 @@ public boolean mergeable(final Message message) { !message.bodyIsHeart() && !this.bodyIsXmpp() && !message.bodyIsXmpp() && - this.isTrusted() == message.isTrusted() + ((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) ); } @@ -818,7 +818,7 @@ public String getFingerprint() { public boolean isTrusted() { FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint); - return s != null && s.isTrustedAndActive(); + return s != null && s.isTrusted(); } private int getPreviousEncryption() { diff --git a/src/main/java/net/atomarea/flowx/generator/AbstractGenerator.java b/src/main/java/net/atomarea/flowx/generator/AbstractGenerator.java index d76ce53d..2191b741 100644 --- a/src/main/java/net/atomarea/flowx/generator/AbstractGenerator.java +++ b/src/main/java/net/atomarea/flowx/generator/AbstractGenerator.java @@ -2,6 +2,13 @@ import android.util.Base64; +import net.atomarea.flowx.Config; +import net.atomarea.flowx.R; +import net.atomarea.flowx.crypto.axolotl.AxolotlService; +import net.atomarea.flowx.services.XmppConnectionService; +import net.atomarea.flowx.utils.PhoneHelper; +import net.atomarea.flowx.xmpp.jingle.stanzas.Content; + import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; @@ -12,13 +19,6 @@ import java.util.Locale; import java.util.TimeZone; -import net.atomarea.flowx.Config; -import net.atomarea.flowx.R; -import net.atomarea.flowx.crypto.axolotl.AxolotlService; -import net.atomarea.flowx.services.XmppConnectionService; -import net.atomarea.flowx.utils.PhoneHelper; -import net.atomarea.flowx.xmpp.jingle.stanzas.Content; - public abstract class AbstractGenerator { private final String[] FEATURES = { "urn:xmpp:jingle:1", @@ -115,9 +115,6 @@ public List getFeatures() { if (!mXmppConnectionService.useTorToConnect()) { features.addAll(Arrays.asList(PRIVACY_SENSITIVE)); } - if (Config.supportOtr()) { - features.addAll(Arrays.asList(OTR)); - } Collections.sort(features); return features; } diff --git a/src/main/java/net/atomarea/flowx/parser/MessageParser.java b/src/main/java/net/atomarea/flowx/parser/MessageParser.java index f88fa75d..9fe3a9a5 100644 --- a/src/main/java/net/atomarea/flowx/parser/MessageParser.java +++ b/src/main/java/net/atomarea/flowx/parser/MessageParser.java @@ -330,7 +330,7 @@ && extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid() } } Message message = null; - if (body != null && body.startsWith("?OTR") && Config.supportOtr()) { + if (body != null && body.startsWith("?OTR")) { if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) { } else { diff --git a/src/main/java/net/atomarea/flowx/persistance/DatabaseBackend.java b/src/main/java/net/atomarea/flowx/persistance/DatabaseBackend.java index abc16a5f..fbc2c5f2 100644 --- a/src/main/java/net/atomarea/flowx/persistance/DatabaseBackend.java +++ b/src/main/java/net/atomarea/flowx/persistance/DatabaseBackend.java @@ -55,7 +55,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; public static final String DATABASE_NAME = "history"; - public static final int DATABASE_VERSION = 32; + public static final int DATABASE_VERSION = 33; private static String START_TIMES_TABLE = "start_times"; private static String CREATE_CONTATCS_STATEMENT = "create table " @@ -361,12 +361,12 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.TRUST + " TEXT"); db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.ACTIVE + " NUMBER"); HashMap migration = new HashMap<>(); - migration.put(0,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNDECIDED,true)); + migration.put(0,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED,true)); migration.put(1,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true)); migration.put(2,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true)); migration.put(3,createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false)); migration.put(4,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); - migration.put(5,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNDECIDED, false)); + migration.put(5,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false)); migration.put(6,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false)); migration.put(7,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true)); migration.put(8,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false)); @@ -375,6 +375,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { String[] where = {String.valueOf(entry.getKey())}; db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,entry.getValue(),whereClause,where); } + } if (oldVersion < 32 && newVersion >= 32) { db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER"); @@ -382,6 +383,10 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION,System.currentTimeMillis()); db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,defaults,null,null); } + if (oldVersion < 33 && newVersion >= 33) { + String whereClause = SQLiteAxolotlStore.OWN+"=1"; + db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED,true),whereClause,null); + } } private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) { @@ -1327,9 +1332,14 @@ public boolean startTimeCountExceedsThreshold() { return count >= Config.FREQUENT_RESTARTS_THRESHOLD; } - public void clearStartTimeCounter() { - Log.d(Config.LOGTAG,"resetting start time counter"); + public void clearStartTimeCounter(boolean justOne) { SQLiteDatabase db = this.getWritableDatabase(); - db.execSQL("delete from "+START_TIMES_TABLE); + if (justOne) { + db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp in (select timestamp from "+START_TIMES_TABLE+" order by timestamp desc limit 1)"); + Log.d(Config.LOGTAG,"do not count start up after being swiped away"); + } else { + Log.d(Config.LOGTAG,"resetting start time counter"); + db.execSQL("delete from " + START_TIMES_TABLE); + } } } \ No newline at end of file diff --git a/src/main/java/net/atomarea/flowx/services/AvatarService.java b/src/main/java/net/atomarea/flowx/services/AvatarService.java index a4165620..3af3f231 100644 --- a/src/main/java/net/atomarea/flowx/services/AvatarService.java +++ b/src/main/java/net/atomarea/flowx/services/AvatarService.java @@ -44,7 +44,9 @@ public AvatarService(XmppConnectionService service) { } private Bitmap get(final Contact contact, final int size, boolean cachedOnly) { - final String KEY = key(contact, size); + if (contact.isSelf()) { + return get(contact.getAccount(),size,cachedOnly); + } final String KEY = key(contact, size); Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); if (avatar != null || cachedOnly) { return avatar; diff --git a/src/main/java/net/atomarea/flowx/services/BarcodeProvider.java b/src/main/java/net/atomarea/flowx/services/BarcodeProvider.java new file mode 100644 index 00000000..01e8321a --- /dev/null +++ b/src/main/java/net/atomarea/flowx/services/BarcodeProvider.java @@ -0,0 +1,192 @@ +package net.atomarea.flowx.services; + +import android.content.ComponentName; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.CancellationSignal; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.aztec.AztecWriter; +import com.google.zxing.common.BitMatrix; + +import net.atomarea.flowx.Config; +import net.atomarea.flowx.entities.Account; +import net.atomarea.flowx.utils.CryptoHelper; +import net.atomarea.flowx.xmpp.jid.Jid; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.Hashtable; + +public class BarcodeProvider extends ContentProvider implements ServiceConnection { + + private static final String AUTHORITY = "net.atomarea.flowx.barcodes"; + + private final Object lock = new Object(); + + private XmppConnectionService mXmppConnectionService; + + @Override + public boolean onCreate() { + File barcodeDirectory = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/"); + if (barcodeDirectory.exists() && barcodeDirectory.isDirectory()) { + for (File file : barcodeDirectory.listFiles()) { + if (file.isFile() && !file.isHidden()) { + Log.d(Config.LOGTAG, "deleting old barcode file " + file.getAbsolutePath()); + file.delete(); + } + } + } + return true; + } + + @Nullable + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + return null; + } + + @Nullable + @Override + public String getType(Uri uri) { + return "image/png"; + } + + @Nullable + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + return openFile(uri, mode, null); + } + + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException { + Log.d(Config.LOGTAG, "opening file with uri (normal): " + uri.toString()); + String path = uri.getPath(); + if (path != null && path.endsWith(".png") && path.length() >= 5) { + String jid = path.substring(1).substring(0, path.length() - 4); + Log.d(Config.LOGTAG, "account:" + jid); + if (connectAndWait()) { + Log.d(Config.LOGTAG, "connected to background service"); + try { + Account account = mXmppConnectionService.findAccountByJid(Jid.fromString(jid)); + if (account != null) { + String shareableUri = account.getShareableUri(); + String hash = CryptoHelper.getFingerprint(shareableUri); + File file = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/" + hash); + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + Bitmap bitmap = createAztecBitmap(account.getShareableUri(), 1024); + OutputStream outputStream = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream); + outputStream.close(); + outputStream.flush(); + } + return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY); + } + } catch (Exception e) { + throw new FileNotFoundException(); + } + } + } + throw new FileNotFoundException(); + } + + private boolean connectAndWait() { + Intent intent = new Intent(getContext(), XmppConnectionService.class); + intent.setAction("contact_chooser"); + Context context = getContext(); + if (context != null) { + context.startService(intent); + context.bindService(intent, this, Context.BIND_AUTO_CREATE); + try { + waitForService(); + Log.d(Config.LOGTAG, "service initialized"); + return true; + } catch (InterruptedException e) { + return false; + } + } else { + Log.d(Config.LOGTAG, "context was null"); + return false; + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service; + mXmppConnectionService = binder.getService(); + synchronized (this.lock) { + lock.notifyAll(); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mXmppConnectionService = null; + } + + private void waitForService() throws InterruptedException { + if (mXmppConnectionService == null) { + synchronized (this.lock) { + lock.wait(); + } + } + } + + public static Uri getUriForAccount(Account account) { + return Uri.parse("content://" + AUTHORITY + "/" + account.getJid().toBareJid() + ".png"); + } + + public static Bitmap createAztecBitmap(String input, int size) { + try { + final AztecWriter AZTEC_WRITER = new AztecWriter(); + final Hashtable hints = new Hashtable<>(); + hints.put(EncodeHintType.ERROR_CORRECTION, 10); + final BitMatrix result = AZTEC_WRITER.encode(input, BarcodeFormat.AZTEC, size, size, hints); + final int width = result.getWidth(); + final int height = result.getHeight(); + final int[] pixels = new int[width * height]; + for (int y = 0; y < height; y++) { + final int offset = y * width; + for (int x = 0; x < width; x++) { + pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE; + } + } + final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + bitmap.setPixels(pixels, 0, width, 0, 0, width, height); + return bitmap; + } catch (final Exception e) { + return null; + } + } +} diff --git a/src/main/java/net/atomarea/flowx/services/XmppConnectionService.java b/src/main/java/net/atomarea/flowx/services/XmppConnectionService.java index bb7ba7a4..b00c4280 100644 --- a/src/main/java/net/atomarea/flowx/services/XmppConnectionService.java +++ b/src/main/java/net/atomarea/flowx/services/XmppConnectionService.java @@ -66,6 +66,7 @@ import net.atomarea.flowx.parser.PresenceParser; import net.atomarea.flowx.persistance.DatabaseBackend; import net.atomarea.flowx.persistance.FileBackend; +import net.atomarea.flowx.ui.SettingsActivity; import net.atomarea.flowx.ui.UiCallback; import net.atomarea.flowx.utils.ConversationsFileObserver; import net.atomarea.flowx.utils.CryptoHelper; @@ -953,7 +954,7 @@ public void onTaskRemoved(final Intent rootIntent) { private void logoutAndSave(boolean stop) { int activeAccounts = 0; - databaseBackend.clearStartTimeCounter(); // regular swipes don't count towards restart counter + databaseBackend.clearStartTimeCounter(true); // regular swipes don't count towards restart counter for (final Account account : accounts) { if (account.getStatus() != Account.State.DISABLED) { activeAccounts++; @@ -1004,7 +1005,7 @@ public XmppConnection createConnection(final Account account) { throw new Exception(); } } catch (Exception e) { - resource = "Pix-Art Messenger"; + resource = "FlowX Messenger"; } account.setResource(resource); final XmppConnection connection = new XmppConnection(account, this); @@ -1103,26 +1104,6 @@ private void sendMessage(final Message message, final boolean resend, final bool } } else { switch (message.getEncryption()) { - case Message.ENCRYPTION_DECRYPTED: - if (!message.needsUploading()) { - String pgpBody = message.getEncryptedBody(); - String decryptedBody = message.getBody(); - message.setBody(pgpBody); - message.setEncryption(Message.ENCRYPTION_PGP); - if (message.edited()) { - message.setBody(decryptedBody); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - databaseBackend.updateMessage(message, message.getEditedId()); - updateConversationUi(); - return; - } else { - databaseBackend.createMessage(message); - saveInDb = false; - message.setBody(decryptedBody); - message.setEncryption(Message.ENCRYPTION_DECRYPTED); - } - } - break; case Message.ENCRYPTION_AXOLOTL: message.setFingerprint(account.getAxolotlService().getOwnFingerprint()); break; @@ -3468,22 +3449,25 @@ public void clearStartTimeCounter() { mDatabaseExecutor.execute(new Runnable() { @Override public void run() { - databaseBackend.clearStartTimeCounter(); + databaseBackend.clearStartTimeCounter(false); } }); } - public void verifyFingerprints(Contact contact, List fingerprints) { + public boolean verifyFingerprints(Contact contact, List fingerprints) { boolean needsRosterWrite = false; + boolean performedVerification = false; final AxolotlService axolotlService = contact.getAccount().getAxolotlService(); for (XmppUri.Fingerprint fp : fingerprints) { if (fp.type == XmppUri.FingerprintType.OTR) { - needsRosterWrite |= contact.addOtrFingerprint(fp.fingerprint); + performedVerification |= contact.addOtrFingerprint(fp.fingerprint); + needsRosterWrite |= performedVerification; } 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()) { + performedVerification = true; axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified()); } } else { @@ -3494,6 +3478,7 @@ public void verifyFingerprints(Contact contact, List finger if (needsRosterWrite) { syncRosterToDisk(contact.getAccount()); } + return performedVerification; } public boolean verifyFingerprints(Account account, List fingerprints) { @@ -3518,6 +3503,10 @@ public boolean verifyFingerprints(Account account, List fin return verifiedSomething; } + public boolean blindTrustBeforeVerification() { + return getPreferences().getBoolean(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, true); + } + public interface OnMamPreferencesFetched { void onPreferencesFetched(Element prefs); diff --git a/src/main/java/net/atomarea/flowx/ui/ContactDetailsActivity.java b/src/main/java/net/atomarea/flowx/ui/ContactDetailsActivity.java index 2ee24581..6766c55c 100644 --- a/src/main/java/net/atomarea/flowx/ui/ContactDetailsActivity.java +++ b/src/main/java/net/atomarea/flowx/ui/ContactDetailsActivity.java @@ -235,6 +235,9 @@ public boolean onCreateOptionsMenu(final Menu menu) { return true; } + public void imageClick(View view) { + shareUri(); + } private void populateView() { if (contact == null) { return; diff --git a/src/main/java/net/atomarea/flowx/ui/EditAccountActivity.java b/src/main/java/net/atomarea/flowx/ui/EditAccountActivity.java index 1e13d362..c6fbc1e8 100644 --- a/src/main/java/net/atomarea/flowx/ui/EditAccountActivity.java +++ b/src/main/java/net/atomarea/flowx/ui/EditAccountActivity.java @@ -480,6 +480,15 @@ private void changePresence() { startActivity(intent); } + private void shareLink(boolean http) { + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("text/plain"); + String text; + text = mAccount.getShareableUri(); + intent.putExtra(Intent.EXTRA_TEXT,text); + startActivity(Intent.createChooser(intent, getText(R.string.share_with))); + } + @Override public void alias(String alias) { if (alias != null) { @@ -507,6 +516,7 @@ private void updateAccountInformation(boolean init) { this.mAvatar.setImageBitmap(avatarService().get(this.mAccount, getPixel(180))); BitmapDrawable bm = getQrCode(); if (bm != null) ((ImageView) findViewById(R.id.iv_cqr)).setImageDrawable(bm); + } if (this.mAccount.isOptionSet(Account.OPTION_REGISTER)) { this.mAccount_info.setVisibility(View.GONE); @@ -521,7 +531,9 @@ private void updateAccountInformation(boolean init) { if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { } } - + public void imageClick(View view) { + shareLink(false); + } public void showRegenerateAxolotlKeyDialog() { Builder builder = new Builder(this); builder.setTitle(R.string.regenerate_omemo_key); diff --git a/src/main/java/net/atomarea/flowx/ui/SettingsActivity.java b/src/main/java/net/atomarea/flowx/ui/SettingsActivity.java index d3d9a238..406715d3 100644 --- a/src/main/java/net/atomarea/flowx/ui/SettingsActivity.java +++ b/src/main/java/net/atomarea/flowx/ui/SettingsActivity.java @@ -32,6 +32,7 @@ public class SettingsActivity extends XmppActivity implements OnSharedPreferenceChangeListener { + public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv"; public static final int REQUEST_WRITE_LOGS = 0xbf8701; private SettingsFragment mSettingsFragment; diff --git a/src/main/java/net/atomarea/flowx/ui/StartConversationActivity.java b/src/main/java/net/atomarea/flowx/ui/StartConversationActivity.java index ec513069..e2b08487 100644 --- a/src/main/java/net/atomarea/flowx/ui/StartConversationActivity.java +++ b/src/main/java/net/atomarea/flowx/ui/StartConversationActivity.java @@ -655,50 +655,9 @@ public boolean onOptionsItemSelected(MenuItem item) { case R.id.action_create_conference: showCreateConferenceDialog(); return true; - case R.id.action_show_qr_code: - AlertDialog.Builder builder = new AlertDialog.Builder(StartConversationActivity.this); - builder.setTitle("QR Scanner"); - builder.setCancelable(true); - View qrView = LayoutInflater.from(builder.getContext()).inflate(R.layout.qr_scanner, null); - final ScannerLiveView scannerLiveView = (ScannerLiveView) qrView.findViewById(R.id.scanner); - scannerLiveView.setScannerViewEventListener(new ScannerLiveView.ScannerViewEventListener() { - @Override - public void onScannerStarted(ScannerLiveView scanner) { - } - - @Override - public void onScannerStopped(ScannerLiveView scanner) { - } - - @Override - public void onScannerError(Throwable err) { - } - - @Override - public void onCodeScanned(String data) { - Uri myUri = Uri.parse(data); - Intent intent = new Intent(Intent.ACTION_VIEW, myUri); - startActivity(intent); - } - }); - builder.setView(qrView); - builder.setNegativeButton("Abbrechen", new OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - scannerLiveView.stopScanner(); - dialog.cancel(); - } - }); - builder.setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - scannerLiveView.stopScanner(); - } - }); - builder.create(); - scannerLiveView.startScanner(); - builder.show(); - break; + case R.id.action_scan_qr_code: + new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE")); + return true; case R.id.action_hide_offline: mHideOfflineContacts = !item.isChecked(); getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit(); diff --git a/src/main/java/net/atomarea/flowx/ui/TrustKeysActivity.java b/src/main/java/net/atomarea/flowx/ui/TrustKeysActivity.java index a7823a53..52aac8af 100644 --- a/src/main/java/net/atomarea/flowx/ui/TrustKeysActivity.java +++ b/src/main/java/net/atomarea/flowx/ui/TrustKeysActivity.java @@ -1,7 +1,12 @@ package net.atomarea.flowx.ui; +import android.app.ActionBar; import android.content.Intent; import android.os.Bundle; +import android.util.Log; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; @@ -10,6 +15,9 @@ import android.widget.TextView; import android.widget.Toast; +import com.google.zxing.integration.android.IntentIntegrator; +import com.google.zxing.integration.android.IntentResult; + import net.atomarea.flowx.Config; import net.atomarea.flowx.OmemoActivity; import net.atomarea.flowx.R; @@ -17,6 +25,7 @@ import net.atomarea.flowx.crypto.axolotl.FingerprintStatus; import net.atomarea.flowx.entities.Account; import net.atomarea.flowx.entities.Conversation; +import net.atomarea.flowx.utils.XmppUri; import net.atomarea.flowx.xmpp.OnKeyStatusUpdated; import net.atomarea.flowx.xmpp.jid.InvalidJidException; import net.atomarea.flowx.xmpp.jid.Jid; @@ -24,6 +33,7 @@ import org.whispersystems.libaxolotl.IdentityKey; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,6 +73,8 @@ public void onClick(View v) { finish(); } }; + private XmppUri mPendingFingerprintVerificationUri = null; + private Toast mUseCameraHintToast = null; @Override protected void refreshUiReal() { @@ -101,6 +113,64 @@ protected void onCreate(final Bundle savedInstanceState) { } } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.trust_keys, menu); + mUseCameraHintToast = Toast.makeText(this,R.string.use_camera_icon_to_scan_barcode,Toast.LENGTH_LONG); + ActionBar actionBar = getActionBar(); + mUseCameraHintToast.setGravity(Gravity.TOP | Gravity.END, 0 ,actionBar == null ? 0 : actionBar.getHeight()); + mUseCameraHintToast.show(); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.action_scan_qr_code: + if (hasPendingKeyFetches()) { + Toast.makeText(this, R.string.please_wait_for_keys_to_be_fetched, Toast.LENGTH_SHORT).show(); + } else { + new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE")); + return true; + } + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent intent) { + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent); + if (scanResult != null && scanResult.getFormatName() != null) { + String data = scanResult.getContents(); + XmppUri uri = new XmppUri(data); + if (xmppConnectionServiceBound) { + processFingerprintVerification(uri); + populateView(); + } else { + this.mPendingFingerprintVerificationUri =uri; + } + } + } + + private void processFingerprintVerification(XmppUri uri) { + if (mConversation != null + && mAccount != null + && uri.hasFingerprints() + && mAccount.getAxolotlService().getCryptoTargets(mConversation).contains(uri.getJid())) { + boolean performedVerification = xmppConnectionService.verifyFingerprints(mAccount.getRoster().getContact(uri.getJid()),uri.getFingerprints()); + boolean keys = reloadFingerprints(); + if (performedVerification && !keys && !hasNoOtherTrustedKeys() && !hasPendingKeyFetches()) { + Toast.makeText(this,R.string.all_omemo_keys_have_been_verified, Toast.LENGTH_SHORT).show(); + finishOk(); + } else if (performedVerification) { + Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show(); + } + } else { + Log.d(Config.LOGTAG,"xmpp uri was: "+uri.getJid()+" has Fingerprints: "+Boolean.toString(uri.hasFingerprints())); + Toast.makeText(this,R.string.barcode_does_not_contain_fingerprints_for_this_conversation,Toast.LENGTH_SHORT).show(); + } + } + private void populateView() { setTitle(getString(R.string.trust_omemo_fingerprints)); ownKeys.removeAllViews(); @@ -215,6 +285,10 @@ public void onBackendConnected() { if (this.mAccount != null && intent != null) { String uuid = intent.getStringExtra("conversation"); this.mConversation = xmppConnectionService.findConversationByUuid(uuid); + if (this.mPendingFingerprintVerificationUri != null) { + processFingerprintVerification(this.mPendingFingerprintVerificationUri); + this.mPendingFingerprintVerificationUri = null; + } reloadFingerprints(); populateView(); } @@ -235,15 +309,22 @@ private boolean hasPendingKeyFetches() { @Override public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) { + final boolean keysToTrust = reloadFingerprints(); if (report != null) { lastFetchReport = report; runOnUiThread(new Runnable() { @Override public void run() { + if (mUseCameraHintToast != null && !keysToTrust) { + mUseCameraHintToast.cancel(); + } switch (report) { case ERROR: Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show(); break; + case SUCCESS_TRUSTED: + Toast.makeText(TrustKeysActivity.this,R.string.blindly_trusted_omemo_keys,Toast.LENGTH_LONG).show(); + break; case SUCCESS_VERIFIED: Toast.makeText(TrustKeysActivity.this, Config.X509_VERIFICATION ? R.string.verified_omemo_key_with_certificate : R.string.all_omemo_keys_have_been_verified, @@ -254,7 +335,6 @@ public void run() { }); } - boolean keysToTrust = reloadFingerprints(); if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) { refreshUi(); } else { diff --git a/src/main/java/net/atomarea/flowx/ui/adapter/MessageAdapter.java b/src/main/java/net/atomarea/flowx/ui/adapter/MessageAdapter.java index 72a9c441..4e21d666 100644 --- a/src/main/java/net/atomarea/flowx/ui/adapter/MessageAdapter.java +++ b/src/main/java/net/atomarea/flowx/ui/adapter/MessageAdapter.java @@ -141,8 +141,8 @@ public int getItemViewType(int position) { private int getMessageTextColor(boolean onDark, boolean primary) { return (onDark ? ContextCompat.getColor(activity, primary ? R.color.black87 : R.color.black54) : ContextCompat.getColor(activity, primary ? R.color.white : R.color.white70)); } + private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground, boolean inValidSession) { - private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) { String filesize = null; String info = null; boolean error = false; @@ -222,8 +222,8 @@ private void displayStatus(ViewHolder viewHolder, Message message, int type, boo .getAccount().getAxolotlService().getFingerprintTrust( message.getFingerprint()); - if(status == null || (!status.isTrustedAndActive())) { - viewHolder.indicator.setColorFilter(activity.getWarningTextColor()); + if(status == null || (!status.isVerified() && inValidSession)) { + viewHolder.indicator.setColorFilter(0xffc64545); viewHolder.indicator.setAlpha(1.0f); } else { viewHolder.indicator.clearColorFilter(); @@ -578,7 +578,8 @@ private void loadMoreMessages(Conversation conversation) { @Override public View getView(int position, View unused, ViewGroup parent) { final Message message = getItem(position); - final boolean isInValidSession = message.isValidInSession(); + final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL; + final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted()); final int type = getItemViewType(position); ViewHolder viewHolder; View view; @@ -712,11 +713,15 @@ public void onClick(View v) { } else { viewHolder.message_box.setBackgroundResource(R.drawable.msg_bbl_warn); viewHolder.encryption.setVisibility(View.VISIBLE); - viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); + if (omemoEncryption && !message.isTrusted()) { + viewHolder.encryption.setText(R.string.not_trusted); + } else { + viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption())); + } } } - displayStatus(viewHolder, message, type, darkBackground); + displayStatus(viewHolder, message, type, darkBackground, isInValidSession); return view; } diff --git a/src/main/java/net/atomarea/flowx/utils/CryptoHelper.java b/src/main/java/net/atomarea/flowx/utils/CryptoHelper.java index dcc94ac3..826c6e70 100644 --- a/src/main/java/net/atomarea/flowx/utils/CryptoHelper.java +++ b/src/main/java/net/atomarea/flowx/utils/CryptoHelper.java @@ -199,6 +199,7 @@ public static Bundle extractCertificateInformation(X509Certificate certificate) } } + public static String getFingerprintCert(byte[] input) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-1"); byte[] fingerprint = md.digest(input); @@ -206,9 +207,13 @@ public static String getFingerprintCert(byte[] input) throws NoSuchAlgorithmExce } public static String getAccountFingerprint(Account account) { + return getFingerprint(account.getJid().toBareJid().toString()); + } + + public static String getFingerprint(String value) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); - return bytesToHex(md.digest(account.getJid().toBareJid().toString().getBytes("UTF-8"))); + return bytesToHex(md.digest(value.getBytes("UTF-8"))); } catch (Exception e) { return ""; } diff --git a/src/main/java/net/atomarea/flowx/utils/XmppUri.java b/src/main/java/net/atomarea/flowx/utils/XmppUri.java index e562cbdb..198182f1 100644 --- a/src/main/java/net/atomarea/flowx/utils/XmppUri.java +++ b/src/main/java/net/atomarea/flowx/utils/XmppUri.java @@ -43,7 +43,7 @@ protected void parse(Uri uri) { String scheme = uri.getScheme(); String host = uri.getHost(); List segments = uri.getPathSegments(); - if ("https".equalsIgnoreCase(scheme) && "conversations.im".equalsIgnoreCase(host)) { + if ("https".equalsIgnoreCase(scheme) && "flowx.im".equalsIgnoreCase(host)) { if (segments.size() >= 2 && segments.get(1).contains("@")) { // sample : https://conversations.im/i/foo@bar.com try { @@ -84,18 +84,18 @@ protected void parse(Uri uri) { protected List parseFingerprints(String query) { List fingerprints = new ArrayList<>(); String[] pairs = query == null ? new String[0] : query.split(";"); - for(String pair : pairs) { - String[] parts = pair.split("=",2); + for (String pair : pairs) { + String[] parts = pair.split("=", 2); if (parts.length == 2) { String key = parts[0].toLowerCase(Locale.US); String value = parts[1].toLowerCase(Locale.US); if (OTR_URI_PARAM.equals(key)) { - fingerprints.add(new Fingerprint(FingerprintType.OTR,value)); + 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)); + fingerprints.add(new Fingerprint(FingerprintType.OMEMO, value, id)); } catch (Exception e) { //ignoring invalid device id } diff --git a/src/main/res/layout/activity_contact_details.xml b/src/main/res/layout/activity_contact_details.xml index 5d3a118e..a16cdac7 100644 --- a/src/main/res/layout/activity_contact_details.xml +++ b/src/main/res/layout/activity_contact_details.xml @@ -133,6 +133,7 @@ diff --git a/src/main/res/layout/activity_edit_account.xml b/src/main/res/layout/activity_edit_account.xml index 3b6ec203..c0eb6ed0 100644 --- a/src/main/res/layout/activity_edit_account.xml +++ b/src/main/res/layout/activity_edit_account.xml @@ -222,6 +222,7 @@ android:visibility="visible" /> diff --git a/src/main/res/layout/activity_trust_keys.xml b/src/main/res/layout/activity_trust_keys.xml index cf14e8e9..ba8d36ec 100644 --- a/src/main/res/layout/activity_trust_keys.xml +++ b/src/main/res/layout/activity_trust_keys.xml @@ -1,9 +1,8 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/grey200"> @@ -32,7 +31,7 @@ android:id="@+id/key_error_message_title" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="?attr/color_text_primary" + android:textColor="@color/black87" android:textSize="?attr/TextSizeHeadline" android:textStyle="bold" android:text="@string/error_trustkeys_title"/> @@ -41,7 +40,7 @@ android:id="@+id/key_error_message" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="?attr/color_text_primary" + android:textColor="@color/black87" android:textSize="?attr/TextSizeBody" android:padding="8dp"/> @@ -55,7 +54,7 @@ android:layout_marginRight="@dimen/activity_horizontal_margin" android:layout_marginTop="@dimen/activity_vertical_margin" android:layout_marginBottom="@dimen/activity_vertical_margin" - android:background="?attr/infocard_border" + android:background="@drawable/infocard_border" android:orientation="vertical" android:padding="@dimen/infocard_padding" android:visibility="gone"> @@ -64,7 +63,7 @@ android:id="@+id/own_keys_title" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="?attr/color_text_primary" + android:textColor="@color/black87" android:textSize="?attr/TextSizeHeadline" android:textStyle="bold"/> @@ -107,14 +106,14 @@ android:layout_height="wrap_content" android:layout_weight="1" android:text="@string/cancel" - android:textColor="?attr/color_text_primary" /> + android:textColor="@color/black87" /> + android:background="@color/black12" />