/* * Copyright 2011 David Brazdil * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package uk.ac.cam.db538.cryptosms.storage; import java.nio.ByteBuffer; import java.util.ArrayList; import org.joda.time.DateTime; import org.joda.time.DateTimeComparator; import org.joda.time.format.DateTimeFormat; import uk.ac.cam.db538.cryptosms.crypto.Encryption; import uk.ac.cam.db538.cryptosms.crypto.EncryptionInterface.EncryptionException; import uk.ac.cam.db538.cryptosms.utils.Charset; import uk.ac.cam.db538.cryptosms.utils.LowLevel; import uk.ac.cam.db538.cryptosms.utils.PhoneNumber; import uk.ac.cam.db538.cryptosms.utils.SimNumber; /** * * Class representing a conversation entry in the secure storage file. * * @author David Brazdil * */ public class Conversation implements Comparable<Conversation> { // FILE FORMAT private static final int LENGTH_FLAGS = 1; private static final int LENGTH_PHONENUMBER = 32; private static final int OFFSET_FLAGS = 0; private static final int OFFSET_PHONENUMBER = OFFSET_FLAGS + LENGTH_FLAGS; private static final int OFFSET_RANDOMDATA = OFFSET_PHONENUMBER + LENGTH_PHONENUMBER; private static final int OFFSET_NEXTINDEX = Storage.ENCRYPTED_ENTRY_SIZE - 4; private static final int OFFSET_PREVINDEX = OFFSET_NEXTINDEX - 4; private static final int OFFSET_MSGSINDEX = OFFSET_PREVINDEX - 4; private static final int OFFSET_KEYSINDEX = OFFSET_MSGSINDEX - 4; private static final int LENGTH_RANDOMDATA = OFFSET_KEYSINDEX - OFFSET_RANDOMDATA; // STATIC private static ArrayList<Conversation> cacheConversation = new ArrayList<Conversation>(); /** * Removes all instances from the list of cached objects. * Be sure you don't use the instances afterwards. */ public static void forceClearCache() { synchronized (cacheConversation) { cacheConversation = new ArrayList<Conversation>(); } } /** * Returns instance of a new Conversation created in one of the empty spaces in file. * * @return the conversation * @throws StorageFileException the storage file exception */ public static Conversation createConversation() throws StorageFileException { // create a new one Conversation conv = new Conversation(Empty.getEmptyIndex(), false); Header.getHeader().attachConversation(conv); Storage.notifyChange(); return conv; } /** * Returns an instance of Conversation class with given index in file. * * @param phoneNumber Contacts phone number * @return the conversation * @throws StorageFileException the storage file exception */ public static Conversation getConversation(String phoneNumber) throws StorageFileException { Conversation conv = Header.getHeader().getFirstConversation(); while (conv != null) { if (PhoneNumber.compare(conv.getPhoneNumber(), phoneNumber)) return conv; conv = conv.getNextConversation(); } return null; } /** * Returns an instance of Conversation class with given index in file. * * @param index Index in file * @return the conversation * @throws StorageFileException the storage file exception */ static Conversation getConversation(long index) throws StorageFileException { if (index <= 0L) return null; // try looking it up synchronized (cacheConversation) { for (Conversation conv: cacheConversation) if (conv.getEntryIndex() == index) return conv; } // create a new one return new Conversation(index, true); } /** * Explicitly requests each conversation in the file to be loaded to memory. * * @throws StorageFileException the storage file exception */ public static void cacheAllConversations() throws StorageFileException { Conversation convCurrent = Header.getHeader().getFirstConversation(); while (convCurrent != null) convCurrent = convCurrent.getNextConversation(); } // INTERNAL FIELDS private long mEntryIndex; // READ ONLY private String mPhoneNumber; private long mIndexSessionKeys; private long mIndexMessages; private long mIndexPrev; private long mIndexNext; // CONSTRUCTORS private Conversation(long index, boolean readFromFile) throws StorageFileException { mEntryIndex = index; if (readFromFile) { byte[] dataEncrypted = Storage.getStorage().getEntry(index); byte[] dataPlain; try { dataPlain = Encryption.getEncryption().decryptSymmetricWithMasterKey(dataEncrypted); } catch (EncryptionException e) { throw new StorageFileException(e); } setPhoneNumber(Charset.fromAscii8(dataPlain, OFFSET_PHONENUMBER, LENGTH_PHONENUMBER)); setIndexSessionKeys(LowLevel.getUnsignedInt(dataPlain, OFFSET_KEYSINDEX)); setIndexMessages(LowLevel.getUnsignedInt(dataPlain, OFFSET_MSGSINDEX)); setIndexPrev(LowLevel.getUnsignedInt(dataPlain, OFFSET_PREVINDEX)); setIndexNext(LowLevel.getUnsignedInt(dataPlain, OFFSET_NEXTINDEX)); } else { // default values setPhoneNumber(""); setIndexSessionKeys(0L); setIndexMessages(0L); setIndexPrev(0L); setIndexNext(0L); saveToFile(); } synchronized (cacheConversation) { cacheConversation.add(this); } } // FUNCTIONS /** * Saves data to the storage file. * * @throws StorageFileException the storage file exception */ public void saveToFile() throws StorageFileException { ByteBuffer convBuffer = ByteBuffer.allocate(Storage.ENCRYPTED_ENTRY_SIZE); // flags byte flags = 0; convBuffer.put(flags); // phone number convBuffer.put(Charset.toAscii8(this.mPhoneNumber, LENGTH_PHONENUMBER)); // random data convBuffer.put(Encryption.getEncryption().generateRandomData(LENGTH_RANDOMDATA)); // indices convBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexSessionKeys)); convBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexMessages)); convBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexPrev)); convBuffer.put(LowLevel.getBytesUnsignedInt(this.mIndexNext)); byte[] dataEncrypted = null; try { dataEncrypted = Encryption.getEncryption().encryptSymmetricWithMasterKey(convBuffer.array()); } catch (EncryptionException e) { throw new StorageFileException(e); } Storage.getStorage().setEntry(this.mEntryIndex, dataEncrypted); } /** * Returns previous instance of Conversation in the double-linked list or null if this is the first. * @return * @throws StorageFileException */ public Conversation getPreviousConversation() throws StorageFileException { return Conversation.getConversation(mIndexPrev); } /** * Returns next instance of Conversation in the double-linked list or null if this is the last. * @return * @throws StorageFileException */ public Conversation getNextConversation() throws StorageFileException { return Conversation.getConversation(mIndexNext); } /** * Returns first SessionKeys object in the stored linked list, or null if there isn't any. * @return * @throws StorageFileException */ public SessionKeys getFirstSessionKeys() throws StorageFileException { if (mIndexSessionKeys == 0) return null; return SessionKeys.getSessionKeys(mIndexSessionKeys); } /** * Attaches new SessionKeys object to the conversation. * Deletes other SessionKeys already attached with the same simNumber. * * @param keys the keys * @throws StorageFileException the storage file exception */ void attachSessionKeys(SessionKeys keys) throws StorageFileException { long indexFirstInStack = getIndexSessionKeys(); if (indexFirstInStack != 0) { SessionKeys firstInStack = SessionKeys.getSessionKeys(indexFirstInStack); firstInStack.setIndexPrev(keys.getEntryIndex()); firstInStack.saveToFile(); } keys.setIndexNext(indexFirstInStack); keys.setIndexPrev(0L); keys.setIndexParent(this.mEntryIndex); keys.saveToFile(); this.setIndexSessionKeys(keys.getEntryIndex()); this.saveToFile(); } /** * Attach new MessageData object to the conversation. * * @param msg the msg * @throws StorageFileException the storage file exception */ void attachMessageData(MessageData msg) throws StorageFileException { long indexFirstInStack = getIndexMessages(); if (indexFirstInStack != 0) { MessageData firstInStack = MessageData.getMessageData(indexFirstInStack); firstInStack.setIndexPrev(msg.getEntryIndex()); firstInStack.saveToFile(); } msg.setIndexNext(indexFirstInStack); msg.setIndexPrev(0L); msg.setIndexParent(this.mEntryIndex); msg.saveToFile(); this.setIndexMessages(msg.getEntryIndex()); this.saveToFile(); } /** * Get the first MessageData object in the linked listed attached to this conversation, or null if there isn't any * @return * @throws StorageFileException */ public MessageData getFirstMessageData() throws StorageFileException { return MessageData.getMessageData(mIndexMessages); } /** * Checks for message data. * * @return true, if successful */ public boolean hasMessageData() { return mIndexMessages != 0; } /** * Get the first MessageData object in the linked listed attached to this conversation, or null if there isn't any * @return * @throws StorageFileException */ public ArrayList<MessageData> getMessages() throws StorageFileException { ArrayList<MessageData> list = new ArrayList<MessageData>(); MessageData msg = getFirstMessageData(); while (msg != null) { list.add(msg); msg = msg.getNextMessageData(); } return list; } /** * Delete MessageData and all the MessageDataParts it controls. * * @throws StorageFileException the storage file exception */ public void delete() throws StorageFileException { Conversation prev = this.getPreviousConversation(); Conversation next = this.getNextConversation(); if (prev != null) { // this is not the first Conversation in the list // update the previous one prev.setIndexNext(this.getIndexNext()); prev.saveToFile(); } else { // this IS the first Conversation in the list // update parent Header header = Header.getHeader(); header.setIndexConversations(this.getIndexNext()); header.saveToFile(); } // update next one if (next != null) { next.setIndexPrev(this.getIndexPrev()); next.saveToFile(); } // delete all of the SessionKeys SessionKeys keys = getFirstSessionKeys(); while (keys != null) { keys.delete(); keys = getFirstSessionKeys(); } // delete all of the MessageDatas MessageData msg = getFirstMessageData(); while (msg != null) { msg.delete(); msg = getFirstMessageData(); } // delete this conversation Empty.replaceWithEmpty(mEntryIndex); // remove from cache synchronized (cacheConversation) { cacheConversation.remove(this); } Storage.notifyChange(); // make this instance invalid this.mEntryIndex = -1L; } /** * Returns session keys assigned to this conversation for specified SIM number, or null if there aren't any. * * @param simNumber the sim number * @return the session keys * @throws StorageFileException the storage file exception */ public SessionKeys getSessionKeys(SimNumber simNumber) throws StorageFileException { SessionKeys keys = getFirstSessionKeys(); while (keys != null) { if (simNumber.equals(keys.getSimNumber())) return keys; keys = keys.getNextSessionKeys(); } return null; } /** * Goes through all the assigned session keys. * If there is a session key with SIM number of the param original, * its SIM number is replaced for the one in param replacement and * all the other keys matching the param replacement are deleted. * If there isn't one matching param original, nothing happens. * * @param original the original * @param replacement the replacement * @throws StorageFileException the storage file exception */ public void replaceSessionKeys(SimNumber original, SimNumber replacement) throws StorageFileException { if (original.equals(replacement)) // no point in continuing return; boolean canBeReplaced = false; boolean sthToDelete = false; SessionKeys keys = this.getFirstSessionKeys(); while (keys != null) { // go through all the assigned keys // look for ones matching the param original if (keys.getSimNumber().equals(original)) // so SIM number of this key should be replaced canBeReplaced = true; if (keys.getSimNumber().equals(replacement)) // this matches the new SIM number // will be deleted if sth is replaced sthToDelete = true; keys = keys.getNextSessionKeys(); } if (canBeReplaced) { if (sthToDelete) { keys = this.getFirstSessionKeys(); while (keys != null) { if (keys.getSimNumber().equals(replacement)) keys.delete(); keys = keys.getNextSessionKeys(); } } boolean found = false; keys = this.getFirstSessionKeys(); while (keys != null) { if (keys.getSimNumber().equals(original)) { // if this is the first key matching original, // its SIM number will be replaced // otherwise deleted because it's redundant if (found) keys.delete(); else { keys.setSimNumber(replacement); keys.saveToFile(); found = true; } } keys = keys.getNextSessionKeys(); } } } /** * Goes through all the SessionKeys assigned with the Conversation * and deletes those that match the simNumber in parameter, * Nothing happens if none are found. * * @param simNumber the sim number * @throws StorageFileException the storage file exception */ public void deleteSessionKeys(SimNumber simNumber) throws StorageFileException { SessionKeys temp, keys = getFirstSessionKeys(); while (keys != null) { temp = keys.getNextSessionKeys(); if (keys.getSimNumber().equals(simNumber)) keys.delete(); keys = temp; } Storage.notifyChange(); } public DateTime getTimeStamp() { MessageData firstMessage = null; try { firstMessage = getFirstMessageData(); } catch (StorageFileException e) { } if (firstMessage != null) return firstMessage.getTimeStamp(); else return new DateTime(); } /** * Returns time in a nice way * @return */ public String getFormattedTime() { return DateTimeFormat.forPattern("HH:mm").print(getTimeStamp()); } /** * Returns whether this conversation should be marked unread * @return */ public boolean getMarkedUnread() { MessageData firstMessage = null; try { firstMessage = getFirstMessageData(); } catch (StorageFileException e) { } if (firstMessage != null) return firstMessage.getUnread(); else return false; } // STATIC FUNCTIONS /** * Calls replaceSessionKeys on all the conversations. * Calls an update of listeners afterwards. * * @param original the original * @param replacement the replacement * @throws StorageFileException the storage file exception */ public static void changeAllSessionKeys(SimNumber original, SimNumber replacement) throws StorageFileException { Conversation conv = Header.getHeader().getFirstConversation(); while (conv != null) { conv.replaceSessionKeys(original, replacement); conv = conv.getNextConversation(); } Storage.notifyChange(); } /** * Returns all SIM numbers stored with session keys of all conversations * @return * @throws StorageFileException */ public static ArrayList<SimNumber> getAllSimNumbersStored() throws StorageFileException { ArrayList<SimNumber> simNumbers = new ArrayList<SimNumber>(); Conversation conv = Header.getHeader().getFirstConversation(); while (conv != null) { SessionKeys keys = conv.getFirstSessionKeys(); while (keys != null) { boolean found = false; for (SimNumber n : simNumbers) if (keys.getSimNumber().equals(n)) found = true; if (!found) simNumbers.add(keys.getSimNumber()); keys = keys.getNextSessionKeys(); } conv = conv.getNextConversation(); } return simNumbers; } /** * Filters list of SIM numbers, looking only for phone numbers. * * @param simNumbers the sim numbers * @return the array list */ public static ArrayList<SimNumber> filterOnlyPhoneNumbers(ArrayList<SimNumber> simNumbers) { ArrayList<SimNumber> phoneNumbers = new ArrayList<SimNumber>(); for (SimNumber n : simNumbers) if (n.isSerial() == false) phoneNumbers.add(n); return phoneNumbers; } /** * Filters list of SIM numbers, removing specified one. * * @param simNumbers the sim numbers * @param filter the filter * @return the array list */ public static ArrayList<SimNumber> filterOutNumber(ArrayList<SimNumber> simNumbers, SimNumber filter) { ArrayList<SimNumber> numbers = new ArrayList<SimNumber>(); for (SimNumber n : simNumbers) if (!n.equals(filter)) numbers.add(n); return numbers; } // GETTERS / SETTERS long getEntryIndex() { return mEntryIndex; } public String getPhoneNumber() { return mPhoneNumber; } public void setPhoneNumber(String phoneNumber) { this.mPhoneNumber = phoneNumber; } long getIndexSessionKeys() { return mIndexSessionKeys; } void setIndexSessionKeys(long indexSessionKyes) { if (indexSessionKyes > 0xFFFFFFFFL || indexSessionKyes < 0L) throw new IndexOutOfBoundsException(); this.mIndexSessionKeys = indexSessionKyes; } long getIndexMessages() { return mIndexMessages; } void setIndexMessages(long indexMessages) { if (indexMessages > 0xFFFFFFFFL || indexMessages < 0L) throw new IndexOutOfBoundsException(); this.mIndexMessages = indexMessages; } long getIndexPrev() { return mIndexPrev; } void setIndexPrev(long indexPrev) { if (indexPrev > 0xFFFFFFFFL || indexPrev < 0L) throw new IndexOutOfBoundsException(); this.mIndexPrev = indexPrev; } long getIndexNext() { return mIndexNext; } void setIndexNext(long indexNext) { if (indexNext > 0xFFFFFFFFL || indexNext < 0L) throw new IndexOutOfBoundsException(); this.mIndexNext = indexNext; } /* (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ @Override public int compareTo(Conversation another) { try { return DateTimeComparator.getInstance().compare(this.getTimeStamp(), another.getTimeStamp()); } catch (Exception e) { return 0; } } }