/* * 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.data; import java.util.ArrayList; import java.util.zip.DataFormatException; import android.content.Context; import uk.ac.cam.db538.cryptosms.MyApplication; import uk.ac.cam.db538.cryptosms.SimCard; import uk.ac.cam.db538.cryptosms.crypto.Encryption; import uk.ac.cam.db538.cryptosms.crypto.EncryptionInterface; import uk.ac.cam.db538.cryptosms.crypto.EncryptionInterface.EncryptionException; import uk.ac.cam.db538.cryptosms.crypto.EncryptionInterface.WrongKeyDecryptionException; import uk.ac.cam.db538.cryptosms.data.PendingParser.ParseResult; import uk.ac.cam.db538.cryptosms.data.PendingParser.PendingParseResult; import uk.ac.cam.db538.cryptosms.storage.Conversation; import uk.ac.cam.db538.cryptosms.storage.MessageData; import uk.ac.cam.db538.cryptosms.storage.SessionKeys; import uk.ac.cam.db538.cryptosms.storage.StorageFileException; import uk.ac.cam.db538.cryptosms.storage.StorageUtils; import uk.ac.cam.db538.cryptosms.utils.CompressedText; import uk.ac.cam.db538.cryptosms.utils.LowLevel; import uk.ac.cam.db538.cryptosms.utils.CompressedText.TextCharset; /* * Class representing an encrypted text message */ public class TextMessage extends Message { // first part specific protected static final int LENGTH_ID = 1; protected static final int OFFSET_ID = OFFSET_HEADER + LENGTH_HEADER; protected static final int LENGTH_INDEX = 1; protected static final int OFFSET_INDEX = OFFSET_ID + LENGTH_ID;; protected static final int OFFSET_DATA = OFFSET_INDEX + LENGTH_INDEX; protected static final int LENGTH_DATA = MessageData.LENGTH_MESSAGE - OFFSET_DATA; private MessageData mStorage; private int mToBeHashed; /** * Instantiates a new text message. * * @param storage the storage */ public TextMessage(MessageData storage) { super(); mStorage = storage; } public MessageData getStorage() { return mStorage; } /** * Returns the length of data stored in the storage file for this message * @return * @throws StorageFileException */ public int getStoredDataLength() throws StorageFileException { int index = 0, length = 0; byte[] temp; try { while ((temp = mStorage.getPartData(index++)) != null) length += temp.length; } catch (IndexOutOfBoundsException e) { // ends this way } return length; } /** * Returns all the data stored in the storage file for this message * @return * @throws StorageFileException */ public byte[] getStoredData() throws StorageFileException { int index = 0, length = 0; byte[] temp; ArrayList<byte[]> data = new ArrayList<byte[]>(); try { while ((temp = mStorage.getPartData(index++)) != null) { length += temp.length; data.add(temp); } } catch (IndexOutOfBoundsException e) { // ends this way } temp = new byte[length]; index = 0; for (byte[] part : data) { System.arraycopy(part, 0, temp, index, part.length); index += part.length; } return temp; } public CompressedText getText() throws StorageFileException, DataFormatException { return CompressedText.decode(getStoredData()); } public uk.ac.cam.db538.cryptosms.storage.MessageData.MessageType getType() { return mStorage.getMessageType(); } public void setText(CompressedText text) throws StorageFileException, MessageException { byte[] data = text.getAlignedData(); // initialise int pos = 0, index = 0, len; int remains = data.length; mStorage.setAscii(text.getCharset() == TextCharset.ASCII); mStorage.setCompressed(text.isCompressed()); mStorage.setNumberOfParts(getMessagePartCount(text.getDataLength())); // save while (remains > 0) { len = Math.min(remains, LENGTH_DATA); mStorage.setPartData(index++, LowLevel.cutData(data, pos, len)); pos += len; remains -= len; } mStorage.saveToFile(); } public int getToBeHashed() { return mToBeHashed; } public void setToBeHashed(int toBeHashed) { mToBeHashed = toBeHashed; } private boolean mKeyIncremented = false; /** * Returns data ready to be sent via SMS * @return * @throws IOException * @throws StorageFileException * @throws MessageException */ @Override public ArrayList<byte[]> getBytes() throws StorageFileException, MessageException, EncryptionException { SessionKeys keys = StorageUtils.getSessionKeysForSim(mStorage.getParent()); if (keys == null) throw new MessageException("No keys found"); mKeyIncremented = false; // get the data, add random data to fit the messages exactly and encrypt it byte[] dataText = getStoredData(); int lengthText = dataText.length; byte[] dataEncrypted = Encryption.getEncryption().encryptSymmetric(dataText, keys.getSessionKey_Out()); int countParts = getMessagePartCount(lengthText); ArrayList<byte[]> listParts = new ArrayList<byte[]>(countParts); int index = 0; byte[] headerAndId = new byte[2]; byte indexByte; Encryption.getEncryption().getRandom().nextBytes(headerAndId); headerAndId[0] &= (byte) 0x3F; // set first two bits to 0 headerAndId[0] |= HEADER_TEXT_FIRST; // set first two bits to HEADER_TEXT_FIRST for (int i = 0; i < countParts; ++i) { int lengthData = Math.min(LENGTH_DATA, dataEncrypted.length - i * LENGTH_DATA); int lengthPart = OFFSET_DATA + lengthData; byte[] dataPart = new byte[lengthPart]; if (index == 0) indexByte = LowLevel.getBytesUnsignedByte(countParts); // first part contains number of parts else indexByte = LowLevel.getBytesUnsignedByte(index); dataPart[OFFSET_HEADER] = headerAndId[0]; dataPart[OFFSET_ID] = headerAndId[1]; dataPart[OFFSET_INDEX] = indexByte; System.arraycopy(dataEncrypted, i * LENGTH_DATA, dataPart, OFFSET_DATA, lengthData); listParts.add(dataPart); index++; headerAndId[0] &= (byte) 0x3F; // set first two bits to 0 headerAndId[0] |= HEADER_TEXT_OTHER; // set first two bits to HEADER_TEXT_OTHER } return listParts; } /* (non-Javadoc) * @see uk.ac.cam.db538.cryptosms.data.Message#sendSMS(java.lang.String, android.content.Context, uk.ac.cam.db538.cryptosms.data.Message.MessageSendingListener) */ @Override public void sendSMS(String phoneNumber, Context context, MessageSendingListener listener) throws StorageFileException, MessageException, EncryptionException { mStorage.setMessageType(uk.ac.cam.db538.cryptosms.storage.MessageData.MessageType.OUTGOING); super.sendSMS(phoneNumber, context, listener); } public static class JoiningException extends Exception { /** * */ private static final long serialVersionUID = 456081152855672327L; private PendingParseResult mReason; /** * Instantiates a new joining exception. * * @param reason the reason */ public JoiningException(PendingParseResult reason) { mReason = reason; } public PendingParseResult getReason() { return mReason; } } protected static byte[] joinParts(ArrayList<Pending> idGroup, int expectedGroupSize) throws JoiningException { // check we have all the parts // there shouldn't be more than 1 int groupSize = idGroup.size(); if (groupSize < expectedGroupSize || groupSize <= 0) throw new JoiningException(PendingParseResult.MISSING_PARTS); else if (groupSize > expectedGroupSize) throw new JoiningException(PendingParseResult.REDUNDANT_PARTS); // get the data byte[][] dataParts = new byte[groupSize][]; int filledParts = 0; for (Pending p : idGroup) { byte[] dataPart = p.getData(); int index = getMessageIndex(dataPart); if (index >= 0 && index < idGroup.size()) { // index is fine, check that there wasn't the same one already if (dataParts[index] == null) { // first time we stumbled upon this index // store the message part data in the array dataParts[index] = dataPart; filledParts++; } else // more parts of the same index throw new JoiningException(PendingParseResult.REDUNDANT_PARTS); } else // index is bigger than the number of messages in ID group // therefore some parts have to be missing or the data is corrupted throw new JoiningException(PendingParseResult.MISSING_PARTS); } // the array was filled with data, so check that there aren't any missing if (filledParts != expectedGroupSize) throw new JoiningException(PendingParseResult.MISSING_PARTS); // lets put the data together byte[] dataJoined = new byte[expectedGroupSize * LENGTH_DATA]; for (int i = 0; i < expectedGroupSize; ++i) { try { // get the data // it can't be too long, thanks to getMessageData // but it can be too short (throws IndexOutOfBounds exception byte[] relevantData = LowLevel.cutData(dataParts[i], OFFSET_DATA, LENGTH_DATA); System.arraycopy(relevantData, 0, dataJoined, i * LENGTH_DATA, LENGTH_DATA); } catch (RuntimeException e) { throw new JoiningException(PendingParseResult.CORRUPTED_DATA); } } return dataJoined; } /** * Parses the text message. * * @param idGroup the id group * @return the parses the result */ public static ParseResult parseTextMessage(ArrayList<Pending> idGroup) { EncryptionInterface crypto = Encryption.getEncryption(); try { // check the sender Contact contact = Contact.getContact(MyApplication.getSingleton().getApplicationContext(), idGroup.get(0).getSender()); if (!contact.existsInDatabase()) return new ParseResult(idGroup, PendingParseResult.UNKNOWN_SENDER, null); String sender = idGroup.get(0).getSender(); // find the keys Conversation conv = Conversation.getConversation(sender); if (conv == null) return new ParseResult(idGroup, PendingParseResult.NO_SESSION_KEYS, null); SessionKeys keys = conv.getSessionKeys(SimCard.getSingleton().getNumber()); if (keys == null) return new ParseResult(idGroup, PendingParseResult.NO_SESSION_KEYS, null); // find the first part and retrieve the length of data int countParts = -1; for (Pending p : idGroup) if (getMessageIndex(p.getData()) == 0) { countParts = getMessagePartCount(p.getData()); break; } if (countParts < 0) // first part not found return new ParseResult(idGroup, PendingParseResult.MISSING_PARTS, null); else if (countParts == 0) // length zero??? return new ParseResult(idGroup, PendingParseResult.CORRUPTED_DATA, null); // join the parts byte[] dataJoined = null; try { dataJoined = joinParts(idGroup, countParts); } catch (JoiningException ex) { return new ParseResult(idGroup, ex.getReason(), null); } // decrypt byte[] dataDecrypted = null; int toBeHashed = 1; int blocksTotalMin = LowLevel.roundUpDivision(Encryption.SYM_OVERHEAD, Encryption.SYM_BLOCK_LENGTH); int blocksMin = Math.max(blocksTotalMin, (countParts - 1) * LENGTH_DATA / Encryption.SYM_BLOCK_LENGTH); int blocksMax = Math.max(blocksTotalMin, countParts * LENGTH_DATA / Encryption.SYM_BLOCK_LENGTH); for (int blocks = blocksMin; blocks <= blocksMax; ++blocks) { toBeHashed = 1; byte[] keyIn = keys.getSessionKey_In(); // try hashing the key until it fits while (dataDecrypted == null && toBeHashed <= 10) { try { dataDecrypted = crypto.decryptSymmetric(dataJoined, keyIn, blocks); } catch (EncryptionException e) { // this is bad return new ParseResult(idGroup, PendingParseResult.INTERNAL_ERROR, null); } catch (WrongKeyDecryptionException e) { // this is OK, we'll just try another one keyIn = crypto.getHash(keyIn); toBeHashed++; } } if (dataDecrypted != null) break; } // was it decrypted? if (dataDecrypted == null) return new ParseResult(idGroup, PendingParseResult.COULD_NOT_DECRYPT, null); // save to conversation MessageData msgData = MessageData.createMessageData(conv); msgData.setMessageType(uk.ac.cam.db538.cryptosms.storage.MessageData.MessageType.INCOMING); TextMessage msgText = new TextMessage(msgData); msgText.setToBeHashed(toBeHashed); try { msgText.setText(CompressedText.decode(dataDecrypted)); } catch (MessageException e) { return new ParseResult(idGroup, PendingParseResult.INTERNAL_ERROR, null); } catch (DataFormatException e) { return new ParseResult(idGroup, PendingParseResult.COULD_NOT_DECRYPT, null); } // looks good, return return new ParseResult(idGroup, PendingParseResult.OK_TEXT_MESSAGE, msgText); } catch (StorageFileException ex) { return new ParseResult(idGroup, PendingParseResult.INTERNAL_ERROR, null); } } /** * Returns message ID for both first and following parts of text messages. * * @param data the data * @return the message id */ public static int getMessageId(byte[] data) { byte[] id = new byte[2]; id[0] = (byte)(data[OFFSET_HEADER] & 0x3F); // ignore first two bits id[1] = data[OFFSET_ID]; return LowLevel.getUnsignedShort(id); } /** * Expects encrypted data of both first and non-first part of text message * and returns its index. * * @param data the data * @return the message index */ public static int getMessageIndex(byte[] data) { if ((data[OFFSET_HEADER] & 0xC0) == HEADER_TEXT_FIRST) return 0; else return LowLevel.getUnsignedByte(data[OFFSET_INDEX]); } /** * Returns how many bytes are left till another message part will be necessary. * * @param lenText the len text * @return the remaining bytes */ public static int getRemainingBytes(int lenText) { int lenComplete = getLengthComplete(lenText); int remainingBytesInBlock = (Encryption.SYM_BLOCK_LENGTH - (lenText % Encryption.SYM_BLOCK_LENGTH)) % Encryption.SYM_BLOCK_LENGTH; int remainingBlocksInMessage = ((LENGTH_DATA - (lenComplete % LENGTH_DATA)) % LENGTH_DATA) / Encryption.SYM_BLOCK_LENGTH; int remainingBytesInMessage = remainingBlocksInMessage * Encryption.SYM_BLOCK_LENGTH; return remainingBytesInBlock + remainingBytesInMessage; } /** * Expects encrypted data of the first part of text message * and returns the data length stored in all the parts * @param data * @return */ protected static int getMessagePartCount(byte[] data) { return LowLevel.getUnsignedByte(data[OFFSET_INDEX]); } private static int getLengthComplete(int lenText) { return Encryption.getEncryption().getSymmetricEncryptedLength(lenText); } /** * Gets the message part count. * * @param lenText the len text * @return the message part count */ public static int getMessagePartCount(int lenText) { return LowLevel.roundUpDivision(getLengthComplete(lenText), LENGTH_DATA); } @Override protected void onMessageSent(String phoneNumber) throws StorageFileException { mStorage.setDeliveredAll(true); mStorage.saveToFile(); } @Override protected void onPartSent(String phoneNumber, int index) throws StorageFileException { mStorage.setPartDelivered(index, true); mStorage.saveToFile(); if (!mKeyIncremented) { // it at least something was sent, increment the ID and session keys SessionKeys keys = StorageUtils.getSessionKeysForSim(this.getStorage().getParent()); if (keys != null) { keys.incrementOut(1); keys.saveToFile(); mKeyIncremented = true; } } } }