Java tutorial
/* * Copyright (c) 2015, Christian Motika. Dedicated to Sara. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright notice, * all contributors, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, an acknowledgment to all contributors, this list of conditions * and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * 3. Neither the name Delphino CryptSecure nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * 4. Free or commercial forks of CryptSecure are permitted as long as * both (a) and (b) are and stay fulfilled: * (a) This license is enclosed. * (b) The protocol to communicate between CryptSecure servers * and CryptSecure clients *MUST* must be fully conform with * the documentation and (possibly updated) reference * implementation from cryptsecure.org. This is to ensure * interconnectivity between all clients and servers. * * THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS AS IS AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * */ package org.cryptsecure; import java.security.Key; import java.security.PrivateKey; import java.security.PublicKey; import java.util.ArrayList; import java.util.List; import javax.crypto.Cipher; import android.annotation.SuppressLint; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.media.RingtoneManager; import android.os.Handler; import android.os.Looper; import android.os.Vibrator; import android.support.v4.app.NotificationCompat; import android.util.Base64; import android.util.Log; /** * The Communicator class handles most of the communication including * sending/receiving messages and receive/read confirmations or the update of * account and session keys. <BR> * <BR> * Notes on AES session keys and update strategy. Keys timeout after one hour. * They timeout a little later for receiving than for sending. Keys can be send * via Internet or SMS. <BR> * Scenario: If sending a key via Internet and the other person currently does * not have Internet connection we should not send encrypted SMS because the * other person would be unable to decrypt.<BR> * Solution: Carry two timestamps for the current key. If sending by Internet * only set the Internet timestamp. If sending by SMS only set the SMS * timestamp. Clear the other transport's timestamp when sending a new key! On * receiving a key: Set BOTH timestamps. If receiving delivery confirmation for * an Internet key message set the SMS timestamp. If receiving delivery * confirmation for an SMS key message then set the Internet timstamp. Now: If * sending a message via Internet and the Internet timestamp is not set, re-send * the key message via Internet (if not outdated). If sending a message via SMS * and the SMS timestamp is not set, re-send the key message via SMS (if not * outdated). This way we ensure that the (non-outdated) key is ALWAYS present * at the receipient no matter transport method is chosen and no matter * connectivity permits SMS or Internet receiving or messages! * * @author Christian Motika * @since 1.2 * @date 08/23/2015 * */ public class Communicator { /** * The message received indicates that one message was received and * potentially more need to be received. */ public static boolean messageReceived = false; /** * The have new messages flag is set to true if the have-request revealed * that there are messages to receive. */ public static boolean haveNewMessages = false; /** The Internet messages queued to be sent. */ public static boolean messagesToSend = false; /** The SMS queued to be sent. */ public static boolean SMSToSend = false; /** * If the class is killed, this flag will be false again it will be set by * the scheduler that evaluates and uses messagesToSend as a cache! This * flag is also set to false if itemToSend != null and hence a message was * (at least tried to be) sent. */ public static boolean messagesToSendIsUpToDate = false; /** * The internetfailcntbarrier after the counter reaches this number the * internet is claimed to fail. */ public static int INTERNETFAILCNTBARRIER = 5; /** * The connection flag tells if Internet was ok. It is evaluated in Main for * the info message. */ public static int internetFailCnt = 0; /** * The loginfailcntbarrier after the counter reaches this number the login * is claimed to fail. */ public static int LOGINFAILCNTBARRIER = 5; /** * The connection flag tells if Login was ok. It is evaluated in Main for * the info message. */ public static int loginFailCnt = 0; /** * The connection flag tells if the account is not activated. It is * evaluated in Main for the info message */ public static boolean accountNotActivated = false; /** The dual to account NOT activated. */ public static boolean accountActivated = false; /** * This flag is a tie-braker and gives alternating priority to Internet and * SMS messages if BOTH have to be sent! This way one of both cannot block * the other. */ public static boolean lastSendInternet = false; /** The key ok separator indicator. */ public static String KEY_OK_SEPARATOR = "@"; /** * The key error separator indicator. If the key is separated by this * separator then the key was sent because a messsage could not be * decrypted. We inform the user and ask him or her to re-send the last * message. */ public static String KEY_ERROR_SEPARATOR = "@@"; public static String NEWLINEESCAPE = "@@@NEWLINE@@@"; public static String HASHESCAPE = "@@@HASH@@@"; public static String GROUPMESSAGEPREFIX = "[GROUP"; public static String GROUPMESSAGEPOSTFIX = "]"; // ---------------------------------------------------------------------------------- /** * This method should help to save communication, it quickly checks for new * messages and only if there are messages waiting it triggers the receive * next message. * * @param context * the context */ public static void haveNewMessagesAndReceive(final Context context, final int serverId) { if (haveNewMessages) { // If we already know we have new messages, we not need to run the // light-weight have-request! receiveNextMessage(context, serverId); return; } // Largest timestamp received final int largestMid = DB.getLargestMid(context, serverId); String sessionid = Setup.getSessionID(context, serverId); String url = null; url = Setup.getBaseURL(context, serverId) + "cmd=have&session=" + sessionid + "&val=" + Utility.urlEncode(largestMid + "#" + DB.getLargestTimestampReceived(context, serverId) + "#" + DB.getLargestTimestampRead(context, serverId)); Log.d("communicator", "REQUEST HAVE: " + url); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, new HttpStringRequest.OnResponseListener() { public void response(String response) { if (isResponseValid(response)) { internetFailCnt = 0; final String response2 = response; // Log.d("communicator", // "RECEIVED NEW MESSAGES WAITING: " // + response2); // Log.d("communicator", "RESPONSE HAVE: " + // response2); if (response.equals("-1")) { // enforce new session if this happens // twice Setup.possiblyInvalidateSession(context, false, serverId); } else if (response2.startsWith("2#")) { Setup.possiblyInvalidateSession(context, true, serverId); // reset // (everythin // ok/normal) // largestMid too high, reduce! - typically this // can only // happen if the msg database on the server is // reset String content = Communicator.getResponseContent(response2); if (content != null && content.length() > 0) { int midFromServer = Utility.parseInt(content, largestMid); Log.d("communicator", "RESPONSE HAVE SET - TOO HIGH MID: " + midFromServer); DB.resetLargestMid(context, midFromServer, serverId); } } else { // NORMAL PROCESSING - 0##...##... or // 1##...##... (the latter means we HAVE // messages to receive!) String responseTail = response2.substring(3); Log.d("communicator", "RESPONSE HAVE PROCESS TAIL: " + responseTail); handleReadAndReceived(context, responseTail, serverId); if (response2.startsWith("1##")) { Setup.possiblyInvalidateSession(context, true, serverId); // reset // (everything // ok/normal) haveNewMessages = true; if (!(Conversation.isVisible() && Conversation.isTyping())) { receiveNextMessage(context, serverId); } } } } else { internetFailCnt++; } } })); } // ---------------------------------------------------------------------------------- /** * Process key deliveries. We must set the key timestamp of the other * transport medium to the value of the one that this key was initially sent * with. Rationale: After a key is delivered to the other partner it is safe * to be used for ANY transport medium. * * @param context * the context * @param hostUid * the host uid */ public static void processKeyDeliveries(final Context context, int senderUid, int midOrLocalId, boolean processSMS) { // If we wait for a key message delivery confirmation in // order to set the other transport's timestamp... // check here: int lastKeyMessageMid = DB.getLastSentKeyMessage(context, senderUid, processSMS); Log.d("communicator", " KEYUPDATE: handleReadAndReceived() lastKeyMessageMid=" + lastKeyMessageMid + " != -1 ???"); if (lastKeyMessageMid > -1) { // -1 means we await in principle but not have a // mid, maybe the key messages is not yet sent // -2 means we do not await such a key message! Log.d("communicator", " KEYUPDATE: handleReadAndReceived() (lastKeyMessageMid == mid) ??? midOrLocalId=" + midOrLocalId); if (lastKeyMessageMid == midOrLocalId) { // Yes... we just received the delivery // confirmation and can NOW use our key for BOTH // transport methods! long timestampInternet = Setup.getAESKeyDate(context, senderUid, DB.TRANSPORT_INTERNET); long timestampSMS = Setup.getAESKeyDate(context, senderUid, DB.TRANSPORT_SMS); Log.d("communicator", " KEYUPDATE: handleReadAndReceived() timestampInternet=" + timestampInternet + ", timestampSMS=" + timestampSMS); // Do not await any more! Utility.saveIntSetting(context, Setup.LASTKEYMID + senderUid, -2); if (timestampInternet > 0) { Log.d("communicator", " KEYUPDATE: handleReadAndReceived() timestampInternet now also saved for SMS"); Setup.setAESKeyDate(context, senderUid, timestampInternet + "", DB.TRANSPORT_SMS); } else { Log.d("communicator", " KEYUPDATE: handleReadAndReceived() timestampSMS now also saved for Internet"); Setup.setAESKeyDate(context, senderUid, timestampSMS + "", DB.TRANSPORT_INTERNET); } } } } // ---------------------------------------------------------------------------------- /** * Handle read and received. * * @param context * the context * @param response * the response */ private static void handleReadAndReceived(final Context context, String response, int serverId) { if (response.equals("0##0")) { // === NO received and NO read messages return; } String[] values = response.split("##"); Log.d("communicator", " UPDATE MESSAGE RECEIVED/READ: valuesSize=" + values.length + ", response=" + response); if (values.length == 2) { String partReceived = values[0]; String partRead = values[1]; String[] valuesReceived = partReceived.split("#"); for (String valueReceived : valuesReceived) { String midAndTs[] = valueReceived.split("@"); Log.d("communicator", " UPDATE MESSAGE RECEIVED: midAndTs[" + valueReceived + "]=" + midAndTs.length); if (midAndTs.length == 2) { int mid = Utility.parseInt(midAndTs[0], -1); int senderUid = DB.getHostUidForMid(context, mid, false); String ts = midAndTs[1]; Log.d("communicator", " UPDATE MESSAGE RECEIVED: mid=" + mid + ", senderUid=" + senderUid + ", serverId=" + serverId); if (mid != -1 && serverId != -1) { DB.updateLargestTimestampReceived(context, ts, serverId); } if (mid != -1 && senderUid != -1) { boolean processSMS = false; processKeyDeliveries(context, senderUid, mid, processSMS); // Only do this AFTER previous processing ... otherwise // we cannot find the lastKeyMessageMid! if (DB.updateGroupMessage(context, mid, senderUid, ts, true, false)) { // This is a group message! senderUid = DB.getLocalgroupuidByMid(context, mid); Log.d("communicator", " UPDATE MESSAGE RECEIVED #1"); // Update successful (any row was modified) // If all are sent|received|read update the // according message int i = DB.isGroupMessage(context, mid, true); Log.d("communicator", " UPDATE MESSAGE RECEIVED #2: (mid " + mid + ") " + i); if (i == DB.GROUPMESSAGE_RECEIVED) { int localidOfGroupMessage = -1 * DB.getLocalidByMid(context, mid); mid = localidOfGroupMessage; } } DB.updateMessageReceived(context, mid, ts, senderUid); updateSentReceivedReadAsync(context, mid, senderUid, false, true, false, false, false); } } } String[] valuesRead = partRead.split("#"); for (String valueRead : valuesRead) { String midAndTs[] = valueRead.split("@"); Log.d("communicator", " UPDATE MESSAGE READ: midAndTs[" + valueRead + "]=" + midAndTs.length); if (midAndTs.length == 2) { int mid = Utility.parseInt(midAndTs[0], -1); int senderUid = DB.getHostUidForMid(context, mid, false); String ts = midAndTs[1]; boolean failed = false; if (ts != null && ts.startsWith("-")) { // This indicates a FAILED TO DECRYPT message => flag // this here // make ts positive, it is the read timestamp still! ts = ts.substring(1); failed = true; } Log.d("communicator", " UPDATE MESSAGE READ: mid=" + mid + ", senderUid=" + senderUid); if (mid != -1 && serverId != -1) { DB.updateLargestTimestampRead(context, ts, serverId); } if (mid != -1 && senderUid != -1) { // ONLY remove the mapping if we know we processed the // read confirmation!!! DB.removeMappingByMid(context, mid); if (failed) { // Flag decryption failed again by using negative // read TS if (DB.updateGroupMessage(context, mid, senderUid, "-" + ts, false, true)) { // This is a group message! senderUid = DB.getLocalgroupuidByMid(context, mid); // Update successful (any row was modified) // If all are sent|received|read update the // according message int i = DB.isGroupMessage(context, mid, true); if (i == DB.GROUPMESSAGE_READ) { int localidOfGroupMessage = -1 * DB.getLocalidByMid(context, mid); mid = localidOfGroupMessage; } } DB.updateMessageRead(context, mid, "-" + ts, senderUid); updateSentReceivedReadAsync(context, mid, senderUid, false, false, false, false, true); } else { // Everything ok if (DB.updateGroupMessage(context, mid, senderUid, ts, false, true)) { // This is a group message! senderUid = DB.getLocalgroupuidByMid(context, mid); // Update successful (any row was modified) // If all are sent|received|read update the // according message int i = DB.isGroupMessage(context, mid, true); if (i == DB.GROUPMESSAGE_READ) { int localidOfGroupMessage = -1 * DB.getLocalidByMid(context, mid); mid = localidOfGroupMessage; } } DB.updateMessageRead(context, mid, ts, senderUid); updateSentReceivedReadAsync(context, mid, senderUid, false, false, true, false, false); } } } } } } // ---------------------------------------------------------------------------------- /** * Update sent received read async. * * @param context * the context * @param mid * the mid * @param hostUid * the host uid * @param sent * the sent * @param received * the received * @param read * the read * @param revoked * the revoked */ public static void updateSentReceivedReadAsync(final Context context, final int mid, final int hostUid, final boolean sent, final boolean received, final boolean read, final boolean revoked, final boolean decryptionfailed) { final Handler mUIHandler = new Handler(Looper.getMainLooper()); mUIHandler.post(new Thread() { @Override public void run() { super.run(); final Handler handler = new Handler(); handler.postDelayed(new Runnable() { public void run() { Log.d("communicator", " GROUP updateSentReceivedReadAsync() #1 " + Conversation.getHostUid() + " == " + hostUid + ", mid=" + mid + ", sent=" + sent + ", received=" + received + ", read=" + read); if (Conversation.isAlive() && (Conversation.getHostUid() == hostUid || hostUid == -1)) { Log.d("communicator", " GROUP updateSentReceivedReadAsync() #2"); if (decryptionfailed) { Conversation.setDecryptionFailed(context, mid); } else if (read) { Conversation.setRead(context, mid); } else if (received) { Conversation.setReceived(context, mid); } else if (sent) { Conversation.setSent(context, mid); } } if (revoked) { Log.d("communicator", "REVOKE GROUP AS SENDER??? updateSentReceivedReadAsync() Conversation.getHostUid()=" + Conversation.getHostUid() + ", hostUid=" + hostUid); if (Conversation.getInstance() != null && (Conversation.getHostUid() == hostUid || hostUid == -1)) { Log.d("communicator", "REVOKE GROUP AS SENDER updateSentReceivedReadAsync() #3 mid=" + mid); Conversation.setRevokedInConversation(context, mid); } // for precaution: clear system notifications // for this user if (Utility.loadBooleanSetting(context, Setup.OPTION_NOTIFICATION, Setup.DEFAULT_NOTIFICATION)) { Communicator.cancelNotification(context, hostUid); } } // If message is sent, we possibly want the update the // userlist if it is visible! if (revoked || sent) { if (Main.getInstance() != null) { // in the revoked case we also want to update // the first line! Main.getInstance().rebuildUserlist(context, false); } } } }, 200); } }); } // ---------------------------------------------------------------------------------- /** * Receive next message. * * @param context * the context */ public synchronized static void receiveNextMessage(final Context context, final int serverId) { messageReceived = false; final int largestMid = DB.getLargestMid(context, serverId); // This should be the case for an empty database only! final boolean discardMessageAndSaveLargestMid = (largestMid == -1); String session = Setup.getTmpLoginEncoded(context, serverId); if (session == null) { // Error resume is automatically done by getTmpLogin, not logged in return; } String url = null; url = Setup.getBaseURL(context, serverId) + "cmd=receive&session=" + session + "&val=" + largestMid; Log.d("communicator", "RECEIVE NEXT MESSAGE: " + url); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, new HttpStringRequest.OnResponseListener() { public void response(String response) { boolean success1 = false; boolean success2 = false; boolean wronglyDencoded = false; boolean resposeError = true; final ConversationItem newItem = new ConversationItem(); if (isResponseValid(response)) { if (response.equals("0")) { haveNewMessages = false; resposeError = false; loginFailCnt = 0; // We currently do not need to look for new // messages } else if (isResponsePositive(response)) { resposeError = false; loginFailCnt = 0; // Log.d("communicator", // "RECEIVE NEXT MESSAGE OK!!! " // + response); // Response should have the form // senttimestamp#mid // Update database String[] responseArray = response.split("#"); if (responseArray.length == 6) { // A message was received String mid = responseArray[1]; String from = Setup.decUid(context, responseArray[2], serverId) + ""; Log.d("communicator", "RECEIVE NEXT MESSAGE DECODED UIDS: " + from + " -> me " + " : " + responseArray[2]); if (from.equals("-2")) { // Enforce new session if this happens // twice wronglyDencoded = true; // we want fast // retry Setup.possiblyInvalidateSession(context, false, serverId); } if (!from.equals("-2")) { // Only not update in case or wrong // decoding because we want to later try // again! DB.updateLargestMid(context, Utility.parseInt(mid, 0), serverId); } if (from.equals("-1")) { // Invalid users // update cache DB.lastReceivedMid.put(serverId, newItem.mid); Utility.saveIntSetting(context, Setup.SETTINGS_DEFAULTMID, newItem.mid); } // Discard unknown or invalid users if (!from.equals("-2") && !from.equals("-1")) { Setup.possiblyInvalidateSession(context, true, serverId); // reset success1 = true; // Uids could be recovered/decrypted String created = responseArray[3]; String sent = responseArray[4]; String text = responseArray[5]; newItem.mid = Utility.parseInt(mid, -1); newItem.from = Utility.parseInt(from, -1); // NEW: recalculate uid from suid got // from server newItem.from = Setup.getUid(context, newItem.from, serverId); Log.d("communicator", "RECEIVE NEXT MESSAGE FROM LOCAL UID: newItem.from=" + newItem.from); newItem.to = DB.myUid(); newItem.created = DB.parseTimestamp(created); newItem.sent = DB.parseTimestamp(sent); newItem.received = DB.getTimestamp(); newItem.transport = DB.TRANSPORT_INTERNET; // Update cache DB.lastReceivedMid.put(serverId, newItem.mid); Utility.saveIntSetting(context, Setup.SETTINGS_DEFAULTMID + serverId, newItem.mid); if (Conversation.isVisible() && Conversation.getHostUid() == newItem.from) { newItem.read = DB.getTimestamp(); ; } // Skip if not in our list && ignore is // turned on OR if this mid is already // in our DB! boolean alreadyInDB = DB.isAlreadyInDB(context, newItem.mid, newItem.from); // Test if the user not exists and if we // want to skip unknown users boolean skipBecauseOfUnknownUser = false; List<Integer> uidList = Main.loadUIDList(context); if (!Main.alreadyInList(newItem.from, uidList)) { // The user who sent us a message is // not // in our list! What now? boolean ignore = Utility.loadBooleanSetting(context, Setup.OPTION_IGNORE, Setup.DEFAULT_IGNORE); // The user must be added to our // list // ATTENTION. from is already // converted from suid to uid! int uid = newItem.from; // Check if this user is manually // ignored! boolean manuallyIgnored = Setup.isIgnored(context, uid); ignore = ignore || manuallyIgnored; // if (manuallyIgnored) { // Utility.showToastAsync( // context, // "Ignored user " // + uid // + // " wrote a message that will be discaded!"); // } if (!ignore) { Main.internalAddUserAndRefreshUserlist(context, uid, "", false); } else { skipBecauseOfUnknownUser = true; } } Log.d("communicator", "RECEIVE NEXT MESSAGE skipBecauseOfUnknownUser=" + skipBecauseOfUnknownUser); if (!skipBecauseOfUnknownUser && !alreadyInDB && !discardMessageAndSaveLargestMid) { String msgText = handleReceivedText(context, text, newItem, serverId); // Handling groups! int start = msgText.indexOf(Communicator.GROUPMESSAGEPREFIX); if (start == 0) { start = GROUPMESSAGEPREFIX.length(); int end = msgText.indexOf(GROUPMESSAGEPOSTFIX, start); String groupSecret = msgText.substring(start, end); Log.d("communicator", "RECEIVED MESSAGE GROUP groupSecret=" + groupSecret); int localGroupId = Setup.getLocalIdBySecret(context, serverId, groupSecret); Log.d("communicator", "RECEIVED MESSAGE GROUP localGroupId=" + localGroupId); msgText = msgText.substring(end + GROUPMESSAGEPOSTFIX.length()); Log.d("communicator", "RECEIVED MESSAGE GROUP msgText=" + msgText); newItem.groupId = localGroupId + ""; // Here we ONLY update the last // message for GROUP messages! // For all NON-group messages // this is already done inside // handleReceivedText()!!! Main.updateLastMessage(context, localGroupId, msgText, newItem.created); success2 = true; } newItem.text = msgText; success2 = updateDBForReceivedMessage(context, newItem) || success2; // Auto-save images possibly (only // if single message or combined!) if (Utility.loadBooleanSetting(context, Setup.OPTION_AUTOSAVE, Setup.DEFAULT_AUTOSAVE) && newItem.readyToProcess()) { MessageDetailsActivity.autoSaveAllImages(context, newItem.text, newItem); } if (newItem.text == null || newItem.text.equals("")) { // No toast or scrolling on // system messages // no notification success2 = false; } // THE FOLLOWING IS ALEADY DONE // SELECTIVELY FOR THIS // USER IN HANDLETEXT! // if (newItem.text // .contains("[ invalid session key ") // || newItem.text // .contains("[ decryption failed ]")) // { // // We should try to update the // // public rsa key of this user // Communicator.updateKeysFromServer( // context, // Main.loadUIDList(context), // true, null, serverId); // } } else { // Discard means no live update // please... success2 = false; } } // End uids decrypted sucessfully } } else { // Invalidate right away Setup.invalidateTmpLogin(context, serverId); loginFailCnt++; Log.d("communicator", "RECEIVE NEXT MESSAGE - LOGIN ERROR!!! " + response); } } // Delayed recursion == try to send a message if (success1 || success2 || wronglyDencoded) { if (success2) { liveUpdateOrNotify(context, newItem); } // Clear errors and possible super fast reschedule Setup.setErrorUpdateInterval(context, false); Scheduler.reschedule(context, false, false, success2); } else { // The error case Scheduler.reschedule(context, true, false, false); } } })); } // --------------------- /** * Update database for received message. * * @param context * the context * @param newItem * the new item * @return true, if successful */ public static synchronized boolean updateDBForReceivedMessage(Context context, ConversationItem newItem) { boolean success2 = false; // THE ADDING OF USERS IS DONE ALREADY IN receiveNextMessage() NOW. // 25.08.15 // AS ALSO ReceiveSMS.handleMessage would create an SMS user. // Skip if an internet message and alrady in our DB if (newItem.transport == DB.TRANSPORT_INTERNET) { boolean alreadyInDB = DB.isAlreadyInDB(context, newItem.mid, newItem.from); if (alreadyInDB) { Log.d("communicator", "RECEIVED MESSAGE SKIPPED BECAUSE OF DUPLICATE!!!"); return false; } } if (DB.isMultipartDuplicate(context, newItem.from, newItem.multipartid)) { Log.d("communicator", "RECEIVED MESSAGE SKIPPED BECAUSE OF DUPLICATE MULTIPARTID!!!"); return false; } int groupId = Utility.parseInt(newItem.groupId, -1); Log.d("communicator", "RECEIVED MESSAGE GROUPID =" + groupId); if (groupId != -1) { // A group item!!! Log.d("communicator", "RECEIVED MESSAGE 2 GROUPID =" + groupId); if (DB.updateMessage(context, newItem, groupId)) { Log.d("communicator", "RECEIVED MESSAGE 3 GROUPID =" + groupId); messageReceived = true; } } else if (DB.updateMessage(context, newItem, newItem.from)) { messageReceived = true; Log.d("communicator", "RECEIVED MESSAGE IN DB NOW!!! from:" + newItem.from); success2 = true; } else { // Log.d("communicator", // "RECEIVED MESSAGE NOT IN DB :( " // + response2); } // Clean up any erroneously received duplicates DB.cleanMultipartDuplicates(context, newItem.from); return success2; } // --------------------- /** * Handle received text. * * @param context * the context * @param text * the text * @param newItem * the new item * @return the string */ public static String handleReceivedText(final Context context, String text, final ConversationItem newItem, int serverId) { if (text.startsWith("W")) { // W == revoke // this is a revoked message followed my the mid to revoke String midString = text.substring(1); // Flag this W-Message as system message DB.updateMessageSystem(context, newItem.mid, true, newItem.from); if (midString != null & midString.length() > 0) { int mid = Utility.parseInt(midString, -1); if (mid > -1) { // REVOKED NOTIFICATION! // now we have correct values and should update the // database int hostUid = newItem.from; int localgroupId = Utility.parseInt(newItem.groupId, -1); Log.d("communicator", "RECEIVED REVOKE MESSAGE GROUPID =" + localgroupId + ", mid=" + mid); if (localgroupId != -1) { hostUid = localgroupId; } newItem.system = true; // The following also will discover groups (if we are the receipient) int localgroupuid = DB.getHostUidForMid(context, mid, true); int localid = -1; // we may be the the sender of a group message, this is the following case int localgroupuidsender = DB.getLocalgroupuidByMid(context, mid); if (localgroupuid == -1) { localgroupuid = localgroupuidsender; } localid = DB.getLocalidByMid(context, mid); // *** Log.d("communicator", "RECEIVED REVOKE MESSAGE GROUP localgroupuid=" + localgroupuid + ", localid=" + localid); if (localgroupuid != -1) { // This MID was part of a group conversation, so go there instead to the user uid hostUid = localgroupuid; } DB.updateMessageRevoked(context, mid, newItem.created + "", localgroupuid); if (localid != -1) { // Additionally do this for the local id (in case of group messages sent, we do not hav // an MID for the locally stored message to be revoked and only have the localid *** DB.updateMessageRevokedByLocalId(context, localid, newItem.created + "", localgroupuid); } Main.updateLastMessage(context, localgroupuid, DB.REVOKEDTEXT, DB.getTimestamp()); Main.updateLastMessage(context, localgroupuidsender, DB.REVOKEDTEXT, DB.getTimestamp()); // If the senderUid will always be the table/hostUid of the // conversation // where the message to revoke resides (in case of non-group // messages). If we sent the // message then // we can look thi up under the mid in this table senderUid // in order to // know if we are the sender int senderUid = DB.getSenderUidByMid(context, mid, hostUid); if (senderUid == DB.myUid()) { senderUid = -1; } if (localgroupuid == -1) { // Non-group case updateSentReceivedReadAsync(context, mid, senderUid, false, false, false, true, false); } else { if (localid != -1) { Log.d("communicator", "RECEIVED REVOKE MESSAGE GROUP AS SENDER mid=" + -1 * localid + ", localgroupuid=" + localgroupuid); // Group case *sender* updateSentReceivedReadAsync(context, -1 * localid, localgroupuidsender, false, false, false, true, false); } else { // Group case *receipient* updateSentReceivedReadAsync(context, mid, localgroupuid, false, false, false, true, false); } } } } text = ""; } else if (text.startsWith("K")) { // This is an RSA encrypted key message! text = text.substring(1); String possiblyInvalid = ""; String keyhash = ""; String separator = KEY_OK_SEPARATOR; String notificationMessageAddition = ""; if (text.contains(KEY_ERROR_SEPARATOR)) { separator = KEY_ERROR_SEPARATOR; notificationMessageAddition = "\n\nThis new key was sent automatically because last message could not be decrypted.\n\nPLEASE RESEND YOUR LAST MESSAGE!"; } // Divide key and signature String[] values = text.split(separator); // Be backwoards compatible here! The timestamp values[2] is NEW and // optional for a while! if (values != null && values.length == 2 || values.length == 3) { String encryptedKey = values[0]; String signature = values[1]; String timestamp = DB.getTimestampString(); if (values.length == 3) { timestamp = values[2]; } // Get encryptedhash from signature PublicKey senderPubKey = Setup.getKey(context, newItem.from); String decryptedKeyHashMustBe = decryptMessage(context, signature, senderPubKey); // Decrypt here PrivateKey myPrivateKey = Setup.getPrivateKey(context); text = decryptMessage(context, encryptedKey, myPrivateKey); // test if signature is valid String decryptedKeyHashIs = Utility.md5(text); // Log.d("communicator", // "@@@@@ SIGNATURE " // + decryptedKeyHashIs // + " SHOUD BE " // + decryptedKeyHashMustBe); if (decryptedKeyHashIs.equals(decryptedKeyHashMustBe)) { // WAIT... before we save the Key we now test the timestamp // if we already have a newer key received by the other // party long tsInternet = Setup.getAESKeyDate(context, newItem.from, DB.TRANSPORT_INTERNET); long tsSMS = Setup.getAESKeyDate(context, newItem.from, DB.TRANSPORT_SMS); long tsThisKey = Utility.parseLong(timestamp, DB.getTimestamp()); if (tsInternet > tsThisKey || tsSMS > tsThisKey) { // Ouuups... it looks like we received an outdated/old // key maybe over a transport medium // that was offline for a while. Note that this could be // SMS when there is no network or // Internet iff the WLAN is down. // We do *NOT* want to override the current key and // discard the outdated one. We state that: possiblyInvalid = "outdated "; Utility.showToastAsync(context, "Received outdated session key" + " from " + Main.UID2Name(context, newItem.from, false) + "."); } else { // Okay the timestamp indicates that this is a newer key // than we ever had: So take it! // Save as AES key for later decryptying and encrypting // usage Setup.saveAESKey(context, newItem.from, text); // When receiving a key then set update timestamps for // both // because we have the current key and could possibly // use it // for both transport ways immediately! Setup.setAESKeyDate(context, newItem.from, DB.getTimestampString(), DB.TRANSPORT_INTERNET); Setup.setAESKeyDate(context, newItem.from, DB.getTimestampString(), DB.TRANSPORT_SMS); keyhash = Setup.getAESKeyHash(context, newItem.from) + " "; // Discard the message Utility.showToastAsync(context, "Received new session key " + keyhash + " from " + Main.UID2Name(context, newItem.from, false) + "."); } } else { possiblyInvalid = "invalid "; Utility.showToastAsync(context, "Received invalid session key from " + Main.UID2Name(context, newItem.from, false) + "."); // Try to receive RSA Key (if we have internet // connection...) Communicator.getKeyFromServer(context, newItem.from, null, serverId); } } else { possiblyInvalid = "invalid "; } newItem.isKey = true; text = "[ " + possiblyInvalid + "session key " + keyhash + "received ]" + notificationMessageAddition; } else if (text.startsWith("U")) { // This is an unencrypted message newItem.encrypted = false; text = text.substring(1); text = text.replace(NEWLINEESCAPE, System.getProperty("line.separator")); text = text.replace(HASHESCAPE, "#"); Log.d("communicator", "QQQQQQ handleReceivedText #1 newItem.from=" + newItem.from + ", newItem.created=" + newItem.created + ", text=" + text); } else if (text.startsWith("E")) { // This is an AES encrypted message newItem.encrypted = true; text = text.substring(1); // Decrypt here Key secretKey = Setup.getAESKey(context, newItem.from); text = decryptAESMessage(context, text, secretKey); if (text == null) { // DECRYPTION with current key failed, now try the backup key we // might have // before claiming this message to be failed to decrypted! Key secretKeyBackup = Setup.getAESKeyBackup(context, newItem.from); if (secretKeyBackup != null) { text = decryptAESMessage(context, text, secretKey); if (text != null) { // OK we could use the backup key... anyways do request // a new key now Communicator.getAESKey(context, newItem.from, true, newItem.transport, true, null, false, true); } } } // FAKE DECRYPTION ERROR HERE // text = null; if (text == null) { // So ... if this still failed, then we might not have the // corrent backup key // or the wrong RSA key?! ... at least we will no tell the // sender that we could // not decrypt his message. He may then decide to resend it! sendSystemMessageFailed(context, newItem.from, newItem.mid); text = "[ decryption failed ]"; int numInvalid = Utility.loadIntSetting(context, "invalidkeycounter" + newItem.from, 0); if (numInvalid == 0) { // TODO: SEND SYSTEM MESSAGE: COULD NOT DECRYPT MID! text = "[ decryption failed ]\n\nAutomatically sending new session key. Other user is asked to resend his last message."; // ONE TIME REQUEST NEW KEY AUTOMATICALLY ... do this only // once because if we have the wrong RSA key this // might trigger a cycle forever!!! // Invalidate the counter Utility.saveIntSetting(context, "invalidkeycounter" + newItem.from, 1); // Try to receive RSA Key (if we have internet // connection...) Communicator.getKeyFromServer(context, newItem.from, null, serverId); // DO THIS AFTER 10 Sek to allow receiving a new RSA key // before .. this is NOT bullet safe! (new Thread(new Runnable() { public void run() { try { Thread.sleep(7000); } catch (InterruptedException e) { } Communicator.getAESKey(context, newItem.from, true, newItem.transport, true, null, false, true); } })).start(); } } else { Log.d("communicator", "QQQQQQ handleReceivedText #2 newItem.from=" + newItem.from + ", newItem.created=" + newItem.created + ", text=" + text); // Reset because no error! Utility.saveIntSetting(context, "invalidkeycounter" + newItem.from, 0); } // PrivateKey myPrivateKey = Setup // .getPrivateKey(context); // text = decryptMessage(context, text, // myPrivateKey); } Log.d("communicator", "MULTIPART START PROCESSING"); // MULTIPART PROCESSING // if (text != null) { DB.MultiPartInfo multipartinfo = DB.getMultiPartInfo(text); if (multipartinfo != null) { // This is a part of a multipart message newItem.part = multipartinfo.part; newItem.parts = multipartinfo.parts; newItem.multipartid = multipartinfo.mulitpartid; newItem.text = multipartinfo.text; // text w/o multipart // info Log.d("communicator", "MULTIPART INFO part=" + newItem.part + ", parts" + newItem.parts + ", multipartid" + newItem.multipartid); if (DB.combineMultiPartMessage(context, newItem)) { // On the LAST part of all, combine all parts! // COMBINING MEANS: // - delete all previously received parts // - combine the text of all into this newItem! // => this newItem MUST be added to the database but // will be afterwards as if received completely here! // it has no multipart information any more, except the // multipartid that is necessary to update the right // conversation item! Main.updateLastMessage(context, newItem.from, newItem.text, newItem.created); Log.d("communicator", "MULTIPART COMBINED !!! :)"); } else { Log.d("communicator", "MULTIPART NOT YET COMBINED :("); } // The text may have been modified by the combine method... // yes this // is a little tricky :-) text = newItem.text; } else { // Only update last line for NON Group message. Reason: For // group message // we first need to figure out which localgroupid this is, which // can be derived from the // secret. But we do not do this in this method. So we must+will // update the lastmessage for group // messages later! if (!text.startsWith(Communicator.GROUPMESSAGEPREFIX)) { // This a NON-multipart message, normal proceed Main.updateLastMessage(context, newItem.from, text, newItem.created); } } } return text; } // --------------------- /** * Live update or notify. * * @param context * the context * @param newItem * the new item */ public static void liveUpdateOrNotify(final Context context, final ConversationItem newItem) { Log.d("communicator", "@@@@ liveUpdateOrNotify() POSSIBLY CREATE NOTIFICATION #1 from=" + newItem.from + ", part=" + newItem.part + ", multipartid=" + newItem.multipartid); // Consider localgroupid as an alternative "from" int uidFromTmp = newItem.from; int localgroupid = Utility.parseInt(newItem.groupId, -1); if (localgroupid != -1) { Log.d("communicator", "@@@@ liveUpdateOrNotify() Conversation.getHostUid()=" + Conversation.getHostUid() + ", localgroupid=" + localgroupid); uidFromTmp = localgroupid; } final int uidFrom = uidFromTmp; final Handler mUIHandler = new Handler(Looper.getMainLooper()); mUIHandler.post(new Thread() { @Override public void run() { super.run(); final Handler handler = new Handler(); handler.postDelayed(new Runnable() { public void run() { if (Conversation.isVisible() || Main.isVisible()) { // Possibly there are new users added and we need to // prompt! Setup.possiblyPromptAutoAddedUser(context); } // Live - update if possible, // otherwise create notification! if (Conversation.isVisible() && Conversation.getHostUid() == uidFrom) { // The conversation is currently // open, try to update this // right away! // // ATTENTION: If not scrolled down then send an // additional toast! if (!Conversation.scrolledDown && !newItem.isKey && newItem.readyToProcess()) { if (newItem.transport == DB.TRANSPORT_INTERNET) { Utility.showToastShortAsync(context, "New message " + newItem.localid + " received."); } else { Utility.showToastShortAsync(context, "New SMS " + newItem.localid + " received."); } } Log.d("communicator", "@@@@ liveUpdateOrNotify() POSSIBLY CREATE NOTIFICATION #A , part=" + newItem.part + ", newItem.multipartid=" + newItem.multipartid); if (!newItem.multipartid.equals(DB.NO_MULTIPART_ID)) { // This IS a multipart message Conversation.hideMultiparts(context, newItem.multipartid); } Conversation.getInstance().updateConversationlist(context); if (!newItem.multipartid.equals(DB.NO_MULTIPART_ID)) { Conversation .setMultipartProgress( context, newItem.multipartid, DB.getPercentReceivedComplete(context, newItem.multipartid, newItem.from, newItem.parts), newItem.text); } } else { // The conversation is NOT // currently // open, we need a new // notification Log.d("communicator", "@@@@ liveUpdateOrNotify() POSSIBLY CREATE NOTIFICATION #2 " + newItem.from); if (!newItem.isKey && newItem.readyToProcess()) { Log.d("communicator", "@@@@ liveUpdateOrNotify() POSSIBLY CREATE NOTIFICATION #3 " + newItem.from); createNotification(context, newItem, uidFrom); } if (Conversation.isAlive() && Conversation.getHostUid() == uidFrom) { // Still update because conversation is in // memory! // Also possibly scroll down: If the user // unlocks the phone, // and scrolledDown was true, then he expects to // see the // last/newest message! if (!newItem.multipartid.equals(DB.NO_MULTIPART_ID)) { // This IS a multipart message Conversation.hideMultiparts(context, newItem.multipartid); } Conversation.getInstance().updateConversationlist(context); if (!newItem.multipartid.equals(DB.NO_MULTIPART_ID)) { Conversation.setMultipartProgress( context, newItem.multipartid, DB.getPercentReceivedComplete(context, newItem.multipartid, newItem.from, newItem.parts), newItem.text); } } } if (Main.isVisible()) { Main.getInstance().rebuildUserlist(context, false); } } }, 200); } }); } // ------------------------------------------------------------------------ // ------------------------------------------------------------------------ /** * Gets the AES key. Returns null to indicate to abort current sending * because a new key was generated and issued. * * @param context * the context * @param uid * the uid * @param forceSendingNewKey * the force sending new key * @param transport * the transport * @param flagErrorMessageNotification * the flag error message notification * @param item * the item * @param forSending * the use of the session key: sending timeout is earlier than * the receivig timeout * @param automatic * the automatic if automatic, then extra countdown is * established * @return the AES key */ public static Key getAESKey(Context context, int uid, boolean forceSendingNewKey, int transport, boolean flagErrorMessageNotification, ConversationItem item, boolean forSending, boolean automatic) { // 1. Search for AES key, if no key (or outdated), then // a. generate one // b. save it for later use // c. sent it first in a K ey message // d. use the new key boolean outDatedInternet = Setup.isAESKeyOutdated(context, uid, forSending, DB.TRANSPORT_INTERNET); boolean outDatedSMS = Setup.isAESKeyOutdated(context, uid, forSending, DB.TRANSPORT_SMS); Log.d("communicator", "#### KEY FOR " + uid + " TIMEOUT? " + (outDatedInternet || outDatedSMS)); boolean outdatedOrNeedsResending = (outDatedInternet && transport == DB.TRANSPORT_INTERNET) || (outDatedSMS && transport == DB.TRANSPORT_SMS); boolean outdatedBoth = outDatedInternet && outDatedSMS; if (!outdatedOrNeedsResending && !forceSendingNewKey) { // Key is up-to-date - AT LEAST for the current transport (hopefully // the other part has it as well!) Key secretKey = Setup.getAESKey(context, uid); Log.d("communicator", "#### KEY RETURNING " + uid + " : " + Setup.getAESKeyHash(context, uid)); return secretKey; } else { Key savedOrNewKey = Setup.getAESKey(context, uid); String savedOrNewKeyAsString = null; long keyTimestamp = DB.getTimestamp(); // Only generate a new key if outdated or forceupdate. // Otherwise re-send the current key by the current media and // don't forget to set the timestamp for this media. if (outdatedBoth || forceSendingNewKey) { // Okay... really a new key is needed... // Use the last received message as a random seed String lastMsg = Main.getLastMessage(context, uid); // Log.d("communicator", "###### RANDOM SEED lastMsg " + // lastMsg); String randomSeed = lastMsg + "-" + DB.getTimestamp() + ""; // Log.d("communicator", "###### RANDOM SEED " + randomSeed); savedOrNewKey = Setup.generateAESKey(randomSeed); savedOrNewKeyAsString = Setup.serializeAESKey(savedOrNewKey); // Save new key Setup.saveAESKey(context, uid, savedOrNewKeyAsString); // Because this is a NEW key now ... we have to // Invalidate key timestamp of OTHER transport method, also // Invalidate DB.getLastKeyMessage() cache set to -1 == // awaiting! Utility.saveIntSetting(context, Setup.LASTKEYMID + uid, -1); if (transport == DB.TRANSPORT_INTERNET) { Log.d("communicator", " KEYUPDATE: getAESKey() invalidate SMS timestamp"); Setup.setAESKeyDate(context, uid, null, DB.TRANSPORT_SMS); } else { Log.d("communicator", " KEYUPDATE: getAESKey() invalidate Internet timestamp"); Setup.setAESKeyDate(context, uid, null, DB.TRANSPORT_INTERNET); } } else { // Okay ... we have a key that is not outdated but we sent it // over to the other person using the OTHER transport medium not // the one we would like to use now. So we should resend the // current key over THIS transport medium. savedOrNewKey = Setup.getAESKey(context, uid); savedOrNewKeyAsString = Setup.serializeAESKey(savedOrNewKey); // Do not await any more! Utility.saveIntSetting(context, Setup.LASTKEYMID + uid, -2); // We need to save the older timestamp of the other transport // here! if (transport == DB.TRANSPORT_INTERNET) { keyTimestamp = Setup.getAESKeyDate(context, uid, DB.TRANSPORT_SMS); } else { keyTimestamp = Setup.getAESKeyDate(context, uid, DB.TRANSPORT_INTERNET); } } if (automatic || true) { // Set extra count down that prevents immediate sending of // the next message in order to allow the key message to be // received before any next message! Setup.extraCrountDownSet(context, transport); } // Save the timestamp of the CURRENT transport Setup.setAESKeyDate(context, uid, keyTimestamp + "", transport); Log.d("communicator", " KEYUPDATE: getAESKey() SAVE NEW KEY FOR TRANPSORT=" + transport); String keyhash = Setup.getAESKeyHash(context, uid); // RSA encrypt here PublicKey pubKeyOther = Setup.getKey(context, uid); String encryptedKey = encryptMessage(context, savedOrNewKeyAsString, pubKeyOther); // Sign the KEY PrivateKey myPrivateKey = Setup.getPrivateKey(context); String keyHash = Utility.md5(savedOrNewKeyAsString); String signature = encryptMessage(context, keyHash, myPrivateKey); String separator = KEY_OK_SEPARATOR; if (flagErrorMessageNotification) { // This indicates the error!!! separator = KEY_ERROR_SEPARATOR; } // NEW: Send a timestamp so that we can rule out outdated keys that // we might receive LATER over // the other transport medium that currently might not work! String msgText = "K" + encryptedKey + separator + signature + separator + DB.getTimestampString(); // Sent a KEY-Message via the same transport as the original message // should go! DB.addSendMessage(context, uid, msgText, false, transport, true, DB.PRIORITY_KEY, item); Conversation.updateConversationlistAsync(context); Communicator.sendNewNextMessageAsync(context, transport); if (transport == DB.TRANSPORT_INTERNET) { Utility.showToastAsync(context, "Sending new session " + keyhash + " key..."); } else { Utility.showToastAsync(context, "Sending new session key " + keyhash + " via SMS..."); } // ATTENTION: // Sending the RSA encrypted KEY-Message before sending the normally // created message ensures that // the symmetric key is received BEFORE the other side tries to // decrypt (possibly with an old key). // If a new key is received, any old one is discarded. // return null to indicate to cancel sending, we sent the key // instead and want to ensure that it arrives earlier than the // message. // The message is left in the send queue. return null; } } // ------------------------------------------------------------------------ // ------------------------------------------------------------------------ /** * Send new next message. This JUST sets the flag that something is in the * sending queue now. This is a hint for the Scheduler that will process the * messages in the sending queue. The Scheduler is responsible for calling * sendNext(). We only need to enqeue it and set the according flag. The * latter is done using this method. * * @param context * the context * @param transport * the transport */ public static void sendNewNextMessageAsync(final Context context, int transport) { if (transport == DB.TRANSPORT_INTERNET) { Communicator.messagesToSend = true; } else { Communicator.SMSToSend = true; } } // ------------------------------------------------------------------------ /** * Send next message. This is NOT called directly but only from the * Scheduler (or if the user selects REFRESH manually). This method will use * the tie-braker if both types of messages are about to send. * * @param context * the context */ public static void sendNextMessage(final Context context) { if ((Conversation.isVisible() && Conversation.isTyping())) { Log.d("communicator", "#### sendNextMessage() SKIPPED DUE TO TYPING " + messagesToSend); // If currently typing, do nothing! return; } else { Log.d("communicator", "#### sendNextMessage() PROCESSING " + messagesToSend); } // This is a cached value. If a message is entered, then this flag is // changed! if (!messagesToSend && !SMSToSend) { Log.d("communicator", "#### sendNextMessage() NO MESSAGES AND NO SMS TO PROCESS... return " + messagesToSend); return; } // Toggle next transport type to send if both types need to be send! // Sometimes one // connection is stuck, then we dont want to get stuck at all! int transport = DB.TRANSPORT_INTERNET; if (messagesToSend && SMSToSend) { if (!lastSendInternet) { } else { transport = DB.TRANSPORT_SMS; } lastSendInternet = !lastSendInternet; } else if (messagesToSend) { transport = DB.TRANSPORT_INTERNET; } else if (SMSToSend) { transport = DB.TRANSPORT_SMS; } // Debugging if (transport == DB.TRANSPORT_INTERNET) { Log.d("communicator", "#### SEND NEXT : TRY INTERNET MESSAGE "); } else { Log.d("communicator", "#### SEND NEXT : TRY SMS MESSAGE "); } // If transport is INTERNET then use the NEXT server that we have // messages // to send for in a Round Robin fashion. If transport is SMS stick with // serverId -1. ConversationItem itemToSend = null; if (transport == DB.TRANSPORT_INTERNET) { // Get the next message considering all servers we have an account // for in a RR style. itemToSend = Setup.getNextSendingServerMessage(context); } else { // Lookup only if we expect something to send itemToSend = DB.getNextMessage(context, DB.TRANSPORT_SMS, -1); } if (itemToSend == null) { if (transport == DB.TRANSPORT_INTERNET) { Communicator.messagesToSend = false; Log.d("communicator", "SEND NEXT QUERY sendNextMessage() DETECTED itemToSend = " + itemToSend + " => messagesToSend:=false"); } else { Log.d("communicator", "SEND NEXT QUERY sendNextMessage() DETECTED itemToSend = " + itemToSend + " => SMStoSend:=false"); Communicator.SMSToSend = false; } } if (itemToSend != null) { if (!itemToSend.isKey && !Setup.extraCountDownToZero(context)) { // If extra countdown is established, we only allow key messages // to // be sent! return; } // Increment the number of tries for this item DB.incrementTries(context, itemToSend.localid); Log.d("communicator", "#### sendNextMessage() SEND NEXT QUERY: localid=" + itemToSend.localid + ", to=" + itemToSend.to + ", created=" + itemToSend.created + ", text=" + itemToSend.text + ", smsfailcnt=" + itemToSend.smsfailcnt); } else { Log.d("communicator", "#### sendNextMessage() SEND NEXT QUERY: IS NULL :("); if (transport == DB.TRANSPORT_INTERNET) { Communicator.messagesToSend = false; } else { Communicator.SMSToSend = false; } } if (itemToSend != null && itemToSend.created > 0 && itemToSend.to != -1) { Log.d("communicator", "#### sendNextMessage() NOW ABOUT TO SEND ... #1"); // If the item wants to be sent unencrypted... well.. do it :( boolean forceUnencrypted = !itemToSend.encrypted; boolean encryption = !forceUnencrypted && Utility.loadBooleanSetting(context, Setup.OPTION_ENCRYPTION, Setup.DEFAULT_ENCRYPTION); boolean haveKey = Setup.haveKey(context, itemToSend.to); Log.d("communicator", "#### sendNextMessage() NOW ABOUT TO SEND ... #2"); String msgText = itemToSend.text; if (itemToSend.system) { // For system messages do not modify the text! } else if (!encryption || !haveKey) { msgText = "U" + msgText; } else { // This fully automatically gets a not-outdated old AES key or // generates a fresh new AES key (also sent) // Use the same transport for the key as the message wants to be // send! // TODO: check if we additionally ALWAYS want to send a key via // Internet? This is still an open question. 8/23/15 Key secretKey = Communicator.getAESKey(context, itemToSend.to, false, itemToSend.transport, false, itemToSend, true, true); if (secretKey == null) { // this indicates that we want to abort sending at this time // because we generated a new key // Fake an error case to allow the key to be retrieved // before this message Setup.setErrorUpdateInterval(context, true); Setup.setErrorUpdateInterval(context, true); Setup.setErrorUpdateInterval(context, true); Setup.setErrorUpdateInterval(context, true); Scheduler.reschedule(context, true, false, false); Log.d("communicator", "#### sendNextMessage() NOW ABOUT TO SEND ... ABORT DUE TO NEW KEY #3"); return; } // encrypt here msgText = "E" + encryptAESMessage(context, msgText, secretKey); // PublicKey pubKeyOther = Setup.getKey(context, itemToSend.to); // msgText = encryptMessage(context, msgText, pubKeyOther); // msgText = "E" + msgText; } Log.d("communicator", "#### sendNextMessage() NOW ABOUT TO SEND ... #4"); sendMessage(context, itemToSend.to, msgText, itemToSend.created, itemToSend, itemToSend.transport); } else if (itemToSend != null) { // Consume message due to error! We MUST consume it - otherwise // following enqueued messages might not get processed. And the // clear protocol is to process them in the order they were // created because order IS important since session keys are also // part of the messages. DB.printDBSending(context); String toastText = "Error sending message " + itemToSend.sendingid + ". Invalid receipient or creation date."; String text = itemToSend.text; if (text != null) { Utility.copyToClipboard(context, text); toastText += " Message text copied to clipboard."; } DB.removeSentMessage(context, itemToSend.sendingid); Utility.showToastAsync(context, toastText); // No more further messages to send for now, this flag is modified // by // Conversation.sendButtonClick! messagesToSend = false; } } // ------------------ // ------------------ // ------------------ /** * Send message. Internal dispatch to Internet or SMS sending. * * @param context * the context * @param to * the to * @param msgText * the msg text * @param created * the created * @param itemToSend * the item to send * @param transport * the transport */ private static void sendMessage(final Context context, final int to, final String msgText, final long created, final ConversationItem itemToSend, final int transport) { // Dispatching for Internet and SMS Message sending if (transport != DB.TRANSPORT_SMS) { Log.d("communicator", "#### sendMessage() INTERNET"); sendMessageInternet(context, to, msgText, created, itemToSend); } else { Log.d("communicator", "#### sendMessage() SMS"); // Utility.showToastAsync(context, "Sending secure SMS ..."); sendMessageSMS(context, to, msgText, created, itemToSend); } } // ------------------------------------------------------------------------ /** * Send message SMS. itemToSend can be null, in this case nothing will be * updated. * * @param context * the context * @param to * the to * @param msgText * the msg text * @param created * the created * @param itemToSend * the item to send */ private static void sendMessageSMS(final Context context, final int to, final String msgText, final long created, final ConversationItem itemToSend) { Log.d("communicator", "#### sendMessageSMS() #1"); String phone = Setup.getPhone(context, to); Log.d("communicator", "#### sendMessageSMS() #2 " + phone); if (phone != null && phone.length() > 0) { String messageTextString = msgText; int localId = -1; int hostUid = -1; int sendingId = -1; int part = DB.DEFAULT_MESSAGEPART; int parts = 1; if (itemToSend != null) { localId = itemToSend.localid; hostUid = itemToSend.to; sendingId = itemToSend.sendingid; part = itemToSend.part; parts = itemToSend.parts; } // Utility.showToastAsync(context, "Sending SMS Message " // +localId+" to " + phone); boolean registeredReceipient = to >= 0; Log.d("communicator", "#### sendMessageSMS() #3 "); SendSMS.sendSMS(context, phone, messageTextString, localId, hostUid, sendingId, part, parts, registeredReceipient); } else { // MUST eliminate message because otherwise next messages will get // stuck... if (itemToSend != null && itemToSend.text != null && itemToSend.sendingid != -1) { Utility.copyToClipboard(context, itemToSend.text); Utility.showToastAsync(context, "Error sending SMS message, no phone number for user " + Main.UID2Name(context, to, false) + ". Message text copied to clipboard."); } DB.removeSentMessage(context, itemToSend.sendingid); } } // ------------------------------------------------------------------------ /** * Send message Internet. itemToSend can be null, in this case nothing will * be updated. * * @param context * the context * @param to * the to * @param msgText * the msg text * @param created * the created * @param itemToSend * the item to send */ private static void sendMessageInternet(final Context context, final int to, final String msgText, final long created, final ConversationItem itemToSend) { // Guard against invalid (SMS) users... if (to < 0) { return; } final int serverId = Setup.getServerId(context, to); String session = Setup.getTmpLogin(context, serverId); if (session == null) { // Error resume is automatically done by getTmpLogin, not logged in return; } String url = null; int toSUid = Setup.getSUid(context, to); String encUid = Setup.encUid(context, toSUid, serverId); if (encUid == null) { // Secret may be not set yet, try again later! return; } Log.d("communicator", "SEND NEXT MESSAGE msgText=" + msgText); url = Setup.getBaseURL(context, serverId) + "cmd=send&session=" + Utility.urlEncode(session) + "&host=" + encUid + "&val=" + Utility.urlEncode(created + "#" + msgText); // // // TEST GET REQUEST JUST TO COMPARE // final String url2222 = Setup.getBaseURL(context) + // "cmd=send&session=" + Utility.encode(session) // + "&host=" + Utility.encode(encUid) + "&val=" + // Utility.encode(created + "#" + msgText); // Log.d("communicator", "SEND NEXT MESSAGE2222: " + url2222); // HttpStringRequest httpStringRequest2222 = (new // HttpStringRequest(context, // url2222, false, new HttpStringRequest.OnResponseListener() { // public void response(String response) { // Log.d("communicator", // "SEND NEXT MESSAGE2222 OK!!! " + response); // } // })); Log.d("communicator", "SEND NEXT MESSAGE: " + url); // Send read confirmations, failed flags, revoke cmd with simple GET // request boolean usePOSTRequest = true; if (msgText.startsWith("R") || msgText.startsWith("F") || msgText.startsWith("A")) { usePOSTRequest = false; } Log.d("communicator", "SEND NEXT MESSAGE: usePOSTRequest=" + usePOSTRequest); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, usePOSTRequest, new HttpStringRequest.OnResponseListener() { public void response(String response) { Log.d("communicator", "SEND NEXT MESSAGE RESPONSE: " + response); boolean success = false; boolean resposeError = true; if (isResponseValid(response)) { if (isResponsePositive(response)) { resposeError = false; String ResponseContent = getResponseContent(response); Log.d("communicator", "SEND NEXT MESSAGE OK!!! " + response); // Response should have the form // senttimestamp#mid // Update database String[] responseArray = ResponseContent.split("#"); if (responseArray.length == 2) { String sent = responseArray[0]; String mid = responseArray[1]; if (itemToSend != null) { // It should not be null even for system // messages! itemToSend.sent = DB.parseTimestamp(sent); itemToSend.mid = Utility.parseInt(mid, -1); // Create a mapping for later matching // uid and mid DB.addMapping(context, itemToSend.mid, itemToSend.to); boolean isSentKeyMessage = itemToSend.me() && itemToSend.isKey; Log.d("communicator", "KEYMESSAGE " + itemToSend.mid + ", " + isSentKeyMessage); // Be careful here! The sending table // may have a different // msg than the msg DB because we might // have a multipart // message. => DO NOT UPDATE THE TEXT! itemToSend.text = null; int groupId = Utility.parseInt(itemToSend.groupId, -1); Log.d("communicator", " GROUP sendMessageInternet() groupId=" + groupId); if (groupId == -1) { // NO group message... noraml update DB.updateMessage(context, itemToSend, itemToSend.to, isSentKeyMessage); } int midInteger = Utility.parseInt(mid, -1); if (groupId != -1) { // Group message non-normal update! DB.updateGroupMessage(context, itemToSend.localid, itemToSend.to, sent, true, itemToSend.mid); } int toUidOrGroupUid = to; Log.d("communicator", " GROUP sendMessageInternet() toUidOrGroupUid=" + toUidOrGroupUid); if (groupId != -1) { int localgroupuid = DB.getLocalgroupuidByMid(context, midInteger); toUidOrGroupUid = localgroupuid; Log.d("communicator", " GROUP sendMessageInternet() toUidOrGroupUid2=" + toUidOrGroupUid); // update sent status ONLY if msg to // ALL members have been sent int i = DB.isGroupMessage(context, midInteger, true); if (i == DB.GROUPMESSAGE_SENT) { int localidOfGroupMessage = -1 * DB.getLocalidByMid(context, midInteger); updateSentReceivedReadAsync(context, localidOfGroupMessage, toUidOrGroupUid, true, false, false, false, false); } } else { updateSentReceivedReadAsync(context, itemToSend.mid, toUidOrGroupUid, true, false, false, false, false); } if (!itemToSend.system) { if (Conversation.isVisible() && Conversation.getHostUid() == toUidOrGroupUid && !Conversation.scrolledDown) { if (itemToSend.transport == DB.TRANSPORT_INTERNET) { Utility.showToastShortAsync(context, "Message " + itemToSend.localid + " sent."); } else { Utility.showToastShortAsync(context, "SMS " + itemToSend.localid + " sent."); } } } DB.removeSentMessage(context, itemToSend.sendingid); } success = true; } } else { Log.d("communicator", "SEND NEXT MESSAGE NOT OK!!! " + response); if (!response.equals("-111")) { // -111 is the // code that the // uid // decryption // failed, just // try again Log.d("communicator", "SEND NEXT MESSAGE NOT OK INVALIDATING!!! " + response); // Something may be wrong with our // session... Setup.possiblyInvalidateSession(context, false, serverId); // Setup.invalidateTmpLogin(context); } else { Setup.possiblyInvalidateSession(context, false, serverId); } } } if (success) { // Clear errors and super fast reschedule Setup.possiblyInvalidateSession(context, true, serverId); // reset Setup.setErrorUpdateInterval(context, false); Scheduler.reschedule(context, false, false, true); } else { // The error case Scheduler.reschedule(context, true, false, false); } } })); } // ------------------------------------------------------------------------- /** * Creates the notification. FromUid can either be a user uid in which case * it should be equal to item.from uid, or it could be a localgroupid in * which case it is equal to item.groupid. * * @param context * the context * @param item * the item * @param fromUid * the from uid */ public static void createNotification(final Context context, ConversationItem item, int fromUid) { boolean vibrate = Utility.loadBooleanSetting(context, Setup.OPTION_VIBRATE, Setup.DEFAULT_VIBRATE); if (vibrate) { if (!Utility.isPhoneMuted(context)) { Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); vibrator.vibrate(200); } } boolean tone = Utility.loadBooleanSetting(context, Setup.OPTION_TONE, Setup.DEFAULT_TONE); if (tone) { if (!Utility.isPhoneMutedOrVibration(context)) { Utility.notfiyAlarm(context, RingtoneManager.TYPE_NOTIFICATION); } } // ALWAYS SET THE NOTIFICATION COUNTER!!! THEN ONLY RETURN FROM THIS // METHOD POSSIBLY IF THE // USER DOES NOT WANT TO SEE A REAL NOTIFICATION setNotificationCount(context, fromUid, false); boolean notification = Utility.loadBooleanSetting(context, Setup.OPTION_NOTIFICATION, Setup.DEFAULT_NOTIFICATION); if (!notification) { return; } NotificationManager notificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); Intent notificationIntent = new Intent(context, TransitActivity.class); notificationIntent = notificationIntent.putExtra(Setup.INTENTEXTRA, fromUid); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); String completeMessage = item.text; if (completeMessage == null) { completeMessage = ""; } String completeTextWithoutImages = Conversation.possiblyRemoveImageAttachments(context, completeMessage, true, "[ image ]", -1); int cnt = getNotificationCount(context, fromUid); String title = Main.UID2Name(context, fromUid, false); String text = completeTextWithoutImages; if (cnt > 1) { text = cnt + " new messages"; } int maxWidth = Utility.getScreenWidth(context) - 80; text = Utility.cutTextIntoOneLine(text, maxWidth, 25); NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context) .setSmallIcon(R.drawable.msgsmall24x24).setPriority(NotificationCompat.PRIORITY_MAX) .setCategory(NotificationCompat.CATEGORY_ALARM).setTicker(title + ": " + completeTextWithoutImages) .setWhen(0).setContentTitle(title).setContentText(text).setContentIntent(pendingIntent) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC); mBuilder.setGroup(Setup.GROUP_CRYPTSECURE); mBuilder.setAutoCancel(true); Notification n = mBuilder.build(); n.contentIntent = pendingIntent; int notificationId = 8888888 + fromUid; notificationManager.notify(notificationId, n); } // ------------------------------------------------------------------------- /** * Cancel notification. * * @param context * the context * @param uid * the uid */ public static void cancelNotification(Context context, int uid) { NotificationManager notificationManager = (NotificationManager) context .getSystemService(Context.NOTIFICATION_SERVICE); int notificationId = 8888888 + uid; notificationManager.cancel(notificationId); } // ------------------------------------------------------------------------- /** * Gets the notification count. * * @param context * the context * @param uid * the uid * @return the notification count */ public static int getNotificationCount(Context context, int uid) { return Utility.loadIntSetting(context, "notification" + uid, 0); } // ------------------------------------------------------------------------ /** * Sets the notification count. reset = true: delete, reset = false: * increment. * * @param context * the context * @param uid * the uid * @param reset * the reset */ public static void setNotificationCount(Context context, int uid, boolean reset) { if (!reset) { Utility.saveIntSetting(context, "notification" + uid, getNotificationCount(context, uid) + 1); } else { Utility.saveIntSetting(context, "notification" + uid, 0); } } // ------------------------------------------------------------------------- /** * Send key to server. * * @param context * the context * @param key * the key * @param keyhash * the keyhash */ public static void sendKeyToServer(final Context context, final String key, final String keyhash, final int serverId, final boolean silent) { String session = Setup.getTmpLoginEncoded(context, serverId); if (session == null) { // Error resume is automatically done by getTmpLogin, not logged in Utility.showToastAsync(context, "Error sending account key " + keyhash + " to " + Setup.getServerLabel(context, serverId, true) + "! (1) - Disabling Encryption!"); // Try to delete keys on all servers Setup.disableEncryption(context, false); return; } String url = null; url = Setup.getBaseURL(context, serverId) + "cmd=sendkey&session=" + session + "&val=" + Utility.urlEncode(key); Log.d("communicator", "###### SEND KEY TO SERVER " + url); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, new HttpStringRequest.OnResponseListener() { public void response(String response) { boolean success = false; if (isResponseValid(response)) { if (response.equals("1")) { success = true; } } if (success) { if (!silent) { Utility.showToastAsync(context, "Account key " + keyhash + " sent to " + Setup.getServerLabel(context, serverId, true) + "."); } } else { Utility.showToastAsync(context, "Error sending account " + keyhash + " key to " + Setup.getServerLabel(context, serverId, true) + "! (2) - Disabling Encryption!"); // Try to delete keys on all servers Setup.disableEncryption(context, false); } } })); } // ------------------------------------------------------------------------- /** * Clear key from server. * * @param context * the context * @param keyhash * the keyhash */ public static void clearKeyFromServer(final Context context, final String keyhash, final int serverId) { String session = Setup.getTmpLoginEncoded(context, serverId); if (session == null) { // Error resume is automatically done by getTmpLogin, not logged in Utility.showToastAsync(context, "Error clearing account " + keyhash + " key from " + Setup.getServerLabel(context, serverId, true) + "! (1)"); return; } String url = null; url = Setup.getBaseURL(context, serverId) + "cmd=clearkey&session=" + session; // Log.d("communicator", "###### CLEAR KEY FROM SERVER " + url); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, new HttpStringRequest.OnResponseListener() { public void response(String response) { boolean success = false; if (isResponseValid(response)) { if (response.equals("1")) { success = true; } } if (success) { Utility.showToastAsync(context, "Account key " + keyhash + " cleared from " + Setup.getServerLabel(context, serverId, true) + "."); } else { Utility.showToastAsync(context, "Error clearing account key " + keyhash + " from " + Setup.getServerLabel(context, serverId, true) + "! (2)"); } } })); } // ------------------------------------------------------------------------- /** * Update keys from ALL servers. This should be done on manual refresh or if * encryption failed for a registered users! * * @param context * the context * @param uidList * the uid list * @param forceUpdate * the force update * @param updateListener * the update listener */ public static void updateKeysFromAllServers(final Context context, final List<Integer> uidList, final boolean forceUpdate, final Main.UpdateListener updateListener) { for (int serverId : Setup.getServerIds(context)) { if (Setup.isServerAccount(context, serverId, false)) { updateKeysFromServer(context, uidList, forceUpdate, updateListener, serverId); } } } // ------------------------------------------------------------------------- /** * Update keys from server. * * @param context * the context * @param uidList * the uid list * @param forceUpdate * the force update * @param updateListener * the update listener */ public static void updateKeysFromServer(final Context context, final List<Integer> uidList, final boolean forceUpdate, final Main.UpdateListener updateListener, final int serverId) { long lastTime = Utility.loadLongSetting(context, Setup.SETTING_LASTUPDATEKEYS + serverId, 0); long currentTime = DB.getTimestamp(); if (!forceUpdate && (lastTime + Setup.UPDATE_KEYS_MIN_INTERVAL + serverId > currentTime)) { // Do not do this more frequently return; } Utility.saveLongSetting(context, Setup.SETTING_LASTUPDATEKEYS + serverId, currentTime); String uidliststring = ""; final ArrayList<Integer> sentList = new ArrayList<Integer>(); for (int uid : uidList) { // not do this of fake UIDs (sms-only users!) or if the user is not // from THIS server! if (uid > 0) { if (Setup.getServerId(context, uid) == serverId) { int suid = Setup.getSUid(context, uid); sentList.add(uid); if (uidliststring.length() != 0) { uidliststring += "#"; } uidliststring += Setup.encUid(context, suid, serverId); } } } String session = Setup.getTmpLoginEncoded(context, serverId); if (session == null) { // error resume is automatically done by getTmpLogin, not logged in return; } String url = null; url = Setup.getBaseURL(context, serverId) + "cmd=haskey&session=" + session + "&val=" + Utility.urlEncode(uidliststring); Log.d("communicator", "###### REQUEST HAS KEY " + url); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, new HttpStringRequest.OnResponseListener() { public void response(String response) { if (isResponseValid(response)) { // Log.d("communicator", // "###### HAS KEY VALUES RECEIVED!!! " // + response); if (isResponsePositive(response)) { String responseContent = getResponseContent(response); List<String> values = Utility.getListFromString(responseContent, "#"); if (values.size() > 0) { boolean updateNeeded = false; int index = 0; for (String value : values) { if (uidList.size() > index) { int uid = sentList.get(index); if (value.equals("0")) { // no key ... delete possibly // old // key if (Setup.haveKey(context, uid)) { updateNeeded = true; Setup.saveKey(context, uid, null, false); Setup.setKeyDate(context, uid, null); } } else { // check if the timestamp // matches or // requires to reload the key! String keydate = Setup.getKeyDate(context, uid); if (keydate == null || !keydate.equals(value)) { Log.d("communicator", "###### KEY TIMESTAMP online = " + value + " != " + keydate + " = cache NOT MATCHING, REQUIRE CURRENT KEY FROM SERVER " + uid); // not matching, update // required, delete old // values // first Setup.saveKey(context, uid, null, false); Setup.setKeyDate(context, uid, null); getKeyFromServer(context, uid, updateListener, serverId); } else { // Log.d("communicator", // "###### KEY UP TO DATE " // + uid); } } } index++; } if (updateNeeded) { Main.possiblyRebuildUserlistAsync(context, false); } } } } } })); } // ------------------------------------------------------------------------- /** * Update avatars from ALL servers. This should only be used on start up or * manual refresh. * * @param context * the context * @param uidList * the uid list * @param forceUpdate * the force update */ public static void updateAvatarFromAllServers(final Context context, final List<Integer> uidList, final boolean forceUpdate) { for (int serverId : Setup.getServerIds(context)) { if (Setup.isServerAccount(context, serverId, false)) { updateAvatarFromServer(context, uidList, forceUpdate, serverId); } } } // ------------------------------------------------------------------------- /** * Update phones from ALL servers. This should only be used on start up or * manual refresh. * * @param context * the context * @param uidList * the uid list * @param forceUpdate * the force update */ public static void updatePhonesFromAllServers(final Context context, final List<Integer> uidList, final boolean forceUpdate) { for (int serverId : Setup.getServerIds(context)) { if (Setup.isServerAccount(context, serverId, false)) { updatePhonesFromServer(context, uidList, forceUpdate, serverId); } } } // ------------------------------------------------------------------------- /** * Update phones from server. * * @param context * the context * @param uidList * the uid list * @param forceUpdate * the force update */ public static void updatePhonesFromServer(final Context context, final List<Integer> uidList, final boolean forceUpdate, final int serverId) { Log.d("communicator", "###### REQUEST HAS PHONE #1"); long lastTime = Utility.loadLongSetting(context, Setup.SETTING_LASTUPDATEPHONES + serverId, 0); long currentTime = DB.getTimestamp(); if (!Setup.isSMSOptionEnabled(context, serverId)) { // if no sms option is enabled, then do not retrieve keys! return; } if (!forceUpdate && (lastTime + Setup.UPDATE_PHONES_MIN_INTERVAL + serverId > currentTime)) { // Do not do this more frequently return; } Utility.saveLongSetting(context, Setup.SETTING_LASTUPDATEPHONES + serverId, currentTime); String uidliststring = ""; final ArrayList<Integer> uidListUsed = new ArrayList<Integer>(); for (int uid : uidList) { // not do this of fake UIDs (sms-only users!) or if this user is not // from THIS server if (uid > 0) { if (Setup.getServerId(context, uid) == serverId) { int suid = Setup.getSUid(context, uid); uidListUsed.add(uid); if (uidliststring.length() != 0) { uidliststring += "#"; } uidliststring += Setup.encUid(context, suid, serverId); } } } if (uidliststring.equals("")) { return; } String session = Setup.getTmpLoginEncoded(context, serverId); if (session == null) { // error resume is automatically done by getTmpLogin, not logged in return; } String url = null; url = Setup.getBaseURL(context, serverId) + "cmd=hasphone&session=" + session + "&val=" + Utility.urlEncode(uidliststring); // Log.d("communicator", "###### REQUEST HAS PHONE (" + uidliststring // + ") " + url); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, new HttpStringRequest.OnResponseListener() { public void response(String response) { if (isResponseValid(response)) { // Log.d("communicator", // "###### HAS PHONE VALUES RECEIVED!!! response=" // + response); if (isResponsePositive(response)) { String responseContent = getResponseContent(response); List<String> values = Utility.getListFromString(responseContent, "#"); if (values.size() > 0) { int index = 0; for (String value : values) { if (uidListUsed.size() > index) { int uid = uidListUsed.get(index); if (value.equals("-1")) { // no phone number or not // elibale // the other person needs to // have you in his userlist in // order // to legitimate you to download // his phone number! this is // a strong privacy requirement if (Main.isUpdatePhone(context, uid)) { Setup.savePhone(context, uid, "", false); } } else { // we are allowed to add this // telephone number locally value = Setup.decText(context, value, serverId); if (value != null) { boolean isUpdate = Main.isUpdatePhone(context, uid); if (isUpdate) { Setup.savePhone(context, uid, value, false); } } // Log.d("communicator", // "###### RESPONSE HAS PHONE SAVE FOR "+Main.UID2Name(context, // uid, false)+": " // + value); } } index++; } } } } } })); } // ------------------------------------------------------------------------- /** * Update pictures from server. * * @param context * the context * @param uidList * the uid list * @param forceUpdate * the force update */ public static void updateAvatarFromServer(final Context context, final List<Integer> uidList, final boolean forceUpdate, final int serverId) { Log.d("communicator", "###### REQUEST HAS AVATAR #1"); long lastTime = Utility.loadLongSetting(context, Setup.SETTING_LASTUPDATEAVATAR + serverId, 0); long currentTime = DB.getTimestamp(); if (!forceUpdate && (lastTime + Setup.UPDATE_AVATAR_MIN_INTERVAL + serverId > currentTime)) { // Do not do this more frequently return; } Utility.saveLongSetting(context, Setup.SETTING_LASTUPDATEAVATAR + serverId, currentTime); String uidliststring = ""; final ArrayList<Integer> uidListUsed = new ArrayList<Integer>(); for (int uid : uidList) { // not do this of fake UIDs (sms-only users!) or if this user is not // from THIS server if (uid > 0) { if (Setup.getServerId(context, uid) == serverId) { int suid = Setup.getSUid(context, uid); uidListUsed.add(uid); if (uidliststring.length() != 0) { uidliststring += "#"; } Log.d("communicator", "###### REQUEST HAS AVATAR UID: " + suid); uidliststring += Setup.encUid(context, suid, serverId); } } } if (uidliststring.equals("")) { return; } String session = Setup.getTmpLoginEncoded(context, serverId); if (session == null) { // error resume is automatically done by getTmpLogin, not logged in return; } String url = null; url = Setup.getBaseURL(context, serverId) + "cmd=hasavatar&session=" + session + "&val=" + Utility.urlEncode(uidliststring); Log.d("communicator", "###### REQUEST HAS AVATAR (" + uidliststring + ") " + url); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, new HttpStringRequest.OnResponseListener() { public void response(String response) { if (isResponseValid(response)) { Log.d("communicator", "###### HAS AVATAR VALUES RECEIVED!!! response=" + response); if (isResponsePositive(response)) { String responseContent = getResponseContent(response); List<String> values = Utility.getListFromString(responseContent, "#"); if (values.size() > 0) { int index = 0; for (String value : values) { if (uidListUsed.size() > index) { int uid = uidListUsed.get(index); if (value.equals("-1")) { // no avatar or not // elibale // the other person needs to // have you in his userlist in // order // to legitimate you to download // his avatar! this is // a strong privacy requirement if (Setup.isUpdateAvatar(context, uid)) { Setup.saveAvatar(context, uid, "", false); } } else { // we are allowed to // download+add this // avatar locally // value = // Setup.decText(context, // value, serverId); if (value != null) { boolean isUpdate = Setup.isUpdateAvatar(context, uid); if (isUpdate) { Setup.saveAvatar(context, uid, value, false); } } // Log.d("communicator", // "###### RESPONSE HAS PHONE SAVE FOR "+Main.UID2Name(context, // uid, false)+": " // + value); } } index++; } } } } } })); } // ------------------------------------------------------------------------- /** * Gets the key from server. * * @param context * the context * @param uid * the uid * @param updateListener * the update listener * @return the key from server */ public static void getKeyFromServer(final Context context, final int uid, final Main.UpdateListener updateListener, final int serverId) { if (uid < 0) { // Do not request keys for SMS/external users! return; } String session = Setup.getTmpLoginEncoded(context, serverId); if (session == null) { // error resume is automatically done by getTmpLogin, not logged in return; } String url = null; int sUid = Setup.getSUid(context, uid); url = Setup.getBaseURL(context, serverId) + "cmd=getkey&session=" + session + "&val=" + Setup.encUid(context, sUid, serverId); Log.d("communicator", "###### REQUEST KEY FOR UID " + uid + " FROM SERVER " + url); final String url2 = url; @SuppressWarnings("unused") HttpStringRequest httpStringRequest = (new HttpStringRequest(context, url2, new HttpStringRequest.OnResponseListener() { public void response(String response) { if (isResponseValid(response)) { // Log.d("communicator", "###### KEY RECEIVED!!! " // + response2); if (isResponsePositive(response)) { String responseContent = getResponseContent(response); List<String> values = Utility.getListFromString(responseContent, "#"); if (values.size() == 2) { Setup.saveKey(context, uid, values.get(1), false); Setup.setKeyDate(context, uid, values.get(0)); Log.d("communicator", "##### KEY SAVED TO CACHE FOR USER " + uid + " timestamp =" + values.get(0) + ", key=" + values.get(1)); Main.possiblyRebuildUserlistAsync(context, false); if (updateListener != null) { updateListener.onUpdate(values.get(1)); } } else { // No key found, delete Setup.saveKey(context, uid, null, false); Main.possiblyRebuildUserlistAsync(context, false); if (updateListener != null) { updateListener.onUpdate(null); } } } } } })); } // ------------------------------------------------------------------------- /** * Decrypt message. * * @param context * the context * @param text * the text * @param key * the key * @return the string */ public static String decryptMessage(Context context, String text, Key key) { // Decode the encoded data with RSA public key byte[] decodedBytes = null; try { Cipher c = Cipher.getInstance("RSA"); c.init(Cipher.DECRYPT_MODE, key); byte[] textAsBytes = Base64.decode(text, Base64.DEFAULT); decodedBytes = c.doFinal(textAsBytes); String returnText = new String(decodedBytes); return returnText; } catch (Exception e) { Log.e("communicator", "RSA decryption error"); e.printStackTrace(); } return null; } // ------------------------------------------------------------------------- /** * Encrypt server message. * * @param context * the context * @param text * the text * @param key * the key * @return the string */ @SuppressLint("TrulyRandom") public static String encryptServerMessage(Context context, String text, Key key) { // Encode the original data with RSA private key byte[] encodedBytes = null; String returnText = null; try { // None // Cipher c = Cipher.getInstance("RSA/None/PKCS1Padding", "BC"); // Cipher c = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC"); Cipher c = Cipher.getInstance("RSA/ECB/PKCS1Padding"); c.init(Cipher.ENCRYPT_MODE, key); encodedBytes = c.doFinal(text.getBytes()); returnText = Base64.encodeToString(encodedBytes, Base64.DEFAULT); } catch (Exception e) { Log.e("communicator", "RSA encryption error"); e.printStackTrace(); } return returnText; } // ------------------------------------------------------------------------- /** * Encrypt message. * * @param context * the context * @param text * the text * @param key * the key * @return the string */ public static String encryptMessage(Context context, String text, Key key) { // Encode the original data with RSA private key byte[] encodedBytes = null; String returnText = null; try { Cipher c = Cipher.getInstance("RSA"); c.init(Cipher.ENCRYPT_MODE, key); encodedBytes = c.doFinal(text.getBytes()); returnText = Base64.encodeToString(encodedBytes, Base64.DEFAULT); } catch (Exception e) { Log.e("communicator", "RSA encryption error"); e.printStackTrace(); } return returnText; } // ------------------------------------------------------------------------- /** * Decrypt aes message. * * @param context * the context * @param text * the text * @param secretKey * the secret key * @return the string */ public static String decryptAESMessage(Context context, String text, Key secretKey) { byte[] decodedBytes = null; try { Cipher c = Cipher.getInstance("AES"); c.init(Cipher.DECRYPT_MODE, secretKey); byte[] textAsBytes = Base64.decode(text, Base64.DEFAULT); decodedBytes = c.doFinal(textAsBytes); String returnText = new String(decodedBytes); return returnText.substring(Setup.RANDOM_STUFF_BYTES); // remove the // 5 random // characters } catch (Exception e) { Log.e("communicator", "AES decryption error"); e.printStackTrace(); } return null; } // ------------------------------------------------------------------------- /** * Encrypt aes message. * * @param context * the context * @param text * the text * @param secretKey * the secret key * @return the string */ public static String encryptAESMessage(Context context, String text, Key secretKey) { // Add 5 random characters just for security text = Utility.getRandomString(Setup.RANDOM_STUFF_BYTES) + text; byte[] encodedBytes = null; String returnText = null; try { Cipher c = Cipher.getInstance("AES"); c.init(Cipher.ENCRYPT_MODE, secretKey); encodedBytes = c.doFinal(text.getBytes()); returnText = Base64.encodeToString(encodedBytes, Base64.DEFAULT); } catch (Exception e) { Log.e("communicator", "AES encryption error"); e.printStackTrace(); } return returnText; } // ------------------------------------------------------------------------- public static void sendReadConfirmation(final Context context, final int uid) { // NO GROUP UID for the conversation read just take the one single // conversation thread of the user int localgroupuid = -1; sendReadConfirmation(context, uid, localgroupuid); } /** * Send read confirmation. * * @param context * the context * @param uid * the uid */ public static void sendReadConfirmation(final Context context, final int uid, final int localgroupuid) { // only send readConfirmation for registered users! if (uid <= 0) { return; } // For a typical conversation the database to get the largest MID from // is the one of the user (uid) // only for groups this database is the one of the localgroupuid. int uiddatabase = uid; if (localgroupuid > -1) { uiddatabase = localgroupuid; } if (Setup.isGroup(context, uid)) { // Do this for ALL members int localGroupId = uid; int serverId = Setup.getGroupServerId(context, localGroupId); String groupId = Setup.getGroupId(context, localGroupId); List<Integer> sUids = Setup.getGroupMembersList(context, serverId, groupId); for (int sUid : sUids) { int realuid = Setup.getUid(context, sUid, serverId); sendReadConfirmation(context, realuid, localGroupId); } return; } // if we do not refuse to send these confirmations... if (!Utility.loadBooleanSetting(context, Setup.OPTION_NOREAD, Setup.DEFAULT_NOREAD)) { // ...then go ahead and send them // send the largest mid for the user watching messages, so that the // server can figure out! // ATTENTION: this largest mid MUST NOT be a system message (R, W) // because we do not send read confirmations for these! (this would // result in a ping pong of read confirmations!) final int mid = DB.getLargestMidForUIDExceptSystemMessages(context, uid, uiddatabase); // wait. first let's see if we have sent this already! int lastMidSent = Utility.loadIntSetting(context, "lastreadconfirmationmid", -1); // Log.d("communicator", // "SEND READ CONFIRMATION ?? lastreadconfirmationmid=" // + lastMidSent + " =?= " + +mid + "=mid"); if (lastMidSent != mid) { sendSystemMessageRead(context, uid, mid); Utility.saveIntSetting(context, "lastreadconfirmationmid", mid); } } } // ------------------------------------------------------------------------- /** * Send system message read. System messages are sent over normal sending * interface, this way they will be sent automatically in order and iff * temporary login is okay. Both system messages will go to the server. * System messages can only go over internet (if available) modes: R == read * (these are processed directly by the server) A == revoke (these are * processed and come also back as W-messages) * * @param context * the context * @param uid * the uid * @param mid * the mid */ public static void sendSystemMessageRead(final Context context, final int uid, final int mid) { // uid = user from which we have read messages // mid = largest mid that we have read if (uid >= 0 && mid != -1) { // if (!Setup.isGroup(context, uid)) { DB.addSendMessage(context, uid, "R" + mid, false, DB.TRANSPORT_INTERNET, true, DB.PRIORITY_READCONFIRMATION); // } else { // // Do this for ALL members // int localGroupId = uid; // int serverId = Setup.getGroupServerId(context, localGroupId); // String groupId = Setup.getGroupId(context, localGroupId); // List<Integer> sUids = Setup.getGroupMembersList(context, // serverId, groupId); // for (int sUid : sUids) { // int realuid = Setup.getUid(context, sUid, serverId); // DB.addSendMessage(context, realuid, "R" + mid, false, // DB.TRANSPORT_INTERNET, true, // DB.PRIORITY_READCONFIRMATION); // } // } Communicator.sendNewNextMessageAsync(context, DB.TRANSPORT_INTERNET); } } // ------------------------------------------------------------------------- /** * Send system message failed. This message will tell the sender that his * message could not be decrypted. System messages are sent over normal * sending interface, this way they will be sent automatically in order and * iff temporary login is okay. Both system messages will go to the server. * System messages can only go over internet (if available) modes: R == read * (these are processed directly by the server) A == revoke (these are * processed and come also back as W-messages) * * @param context * the context * @param uid * the uid * @param mid * the mid */ public static void sendSystemMessageFailed(final Context context, final int uid, final int mid) { // uid = user from which we have read messages // mid = largest mid that we have read if (uid >= 0 && mid != -1) { DB.addSendMessage(context, uid, "F" + mid, false, DB.TRANSPORT_INTERNET, true, DB.PRIORITY_FAILEDTODECRYPT); Communicator.sendNewNextMessageAsync(context, DB.TRANSPORT_INTERNET); } } // ------------------------------------------------------------------------- /** * Send system message widthdraw. System messages are sent over normal * sending interface, this way they will be sent automatically in order and * iff temporary login is okay. Both system messages will go to the server. * System messages can only go over internet (if available) modes: R == read * (these are processed directly by the server) A == revoke (these are * processed and come also back as W-messages) * * @param context * the context * @param uid * the uid * @param mid * the mid */ public static void sendSystemMessageRevoke(final Context context, final int uid, final int mid) { Utility.showToastAsync(context, "Revoke request for message " + mid + "..."); if (uid >= 0 && mid != -1) { DB.addSendMessage(context, uid, "A" + mid, false, DB.TRANSPORT_INTERNET, true, DB.PRIORITY_KEY); Communicator.sendNewNextMessageAsync(context, DB.TRANSPORT_INTERNET); } } // ------------------------------------------------------------------------- /** * Checks if is response is a number or starts with a number followed by a * #. * * @param response * the response * @return true, if is response ok/valid */ public static boolean isResponseValid(String response) { if (response == null || response.length() == 0) { // no info received return false; } if (Utility.parseInt(response, Integer.MIN_VALUE) != Integer.MIN_VALUE) { // is a number return true; } int i = response.indexOf("#"); if (i < 1) { // no # found return false; } String partResponse = response.substring(0, i); if (Utility.parseInt(partResponse, Integer.MIN_VALUE) != Integer.MIN_VALUE) { // first part is a number return true; } return false; } // ------------------------------------------------------------------------- /** * Checks if the response does not start with a negative number!. * * @param response * the response * @return true, if successful */ public static boolean isResponsePositive(String response) { return !response.startsWith("-"); } // ------------------------------------------------------------------------- /** * Gets the response content, i.e., everything after the first number, or * null iff there is no such content. * * @param response * the response * @return the response content */ public static String getResponseContent(String response) { int i = response.indexOf("#"); if (i < 1) { // no # found, no content return null; } return response.substring(i + 1); } // ------------------------------------------------------------------------- }