/* * 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.util.ArrayList; import uk.ac.cam.db538.cryptosms.crypto.EllipticCurveDeffieHellman; 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.SimNumber; /** * * Class representing a session keys entry in the secure storage file. * * @author David Brazdil * */ public class SessionKeys { // FILE FORMAT private static final int LENGTH_FLAGS = 1; private static final int LENGTH_SIMNUMBER = 32; private static final int LENGTH_SESSIONKEY = Encryption.SYM_KEY_LENGTH; private static final int LENGTH_PRIVATE_KEY = EllipticCurveDeffieHellman.LENGTH_PRIVATE_KEY; private static final int LENGTH_TIMESTAMP = 8; private static final int OFFSET_FLAGS = 0; private static final int OFFSET_SIMNUMBER = OFFSET_FLAGS + LENGTH_FLAGS; private static final int OFFSET_SESSIONKEY_OUTGOING = OFFSET_SIMNUMBER + LENGTH_SIMNUMBER; private static final int OFFSET_SESSIONKEY_INCOMING = OFFSET_SESSIONKEY_OUTGOING + LENGTH_SESSIONKEY; private static final int OFFSET_PRIVATE_KEY = OFFSET_SESSIONKEY_INCOMING + LENGTH_SESSIONKEY; private static final int OFFSET_TIMESTAMP = OFFSET_PRIVATE_KEY + LENGTH_PRIVATE_KEY; private static final int OFFSET_RANDOMDATA = OFFSET_TIMESTAMP + LENGTH_TIMESTAMP; 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_PARENTINDEX = OFFSET_PREVINDEX - 4; private static final int LENGTH_RANDOMDATA = OFFSET_PARENTINDEX - OFFSET_RANDOMDATA; private static final int KEYS_VALID_FOR = 365 * 24 * 3600 * 1000; // one year // STATIC private static ArrayList<SessionKeys> cacheSessionKeys = new ArrayList<SessionKeys>(); /** * Removes all instances from the list of cached objects. * Be sure you don't use the instances afterwards. */ public static void forceClearCache() { synchronized (cacheSessionKeys) { cacheSessionKeys = new ArrayList<SessionKeys>(); } } /** * Returns a new instance of the SessionKeys class, which replaces an empty entry in the file. * * @param parent the parent * @return the session keys * @throws StorageFileException the storage file exception */ public static SessionKeys createSessionKeys(Conversation parent) throws StorageFileException { SessionKeys keys = new SessionKeys(Empty.getEmptyIndex(), false); parent.attachSessionKeys(keys); return keys; } /** * Returns a new instance of the SessionKeys class, which represents a given entry in the file. * * @param index Index in file * @return the session keys * @throws StorageFileException the storage file exception */ static SessionKeys getSessionKeys(long index) throws StorageFileException { if (index <= 0L) return null; // try looking it up synchronized (cacheSessionKeys) { for (SessionKeys keys: cacheSessionKeys) if (keys.getEntryIndex() == index) return keys; } // create a new one return new SessionKeys(index, true); } // INTERNAL FIELDS private long mEntryIndex; // READ ONLY private boolean mKeysSent; private boolean mKeysConfirmed; private SimNumber mSimNumber; private byte[] mSessionKey_Out; private byte[] mSessionKey_In; private byte[] mPrivateKey; private long mTimeStamp; private long mIndexParent; private long mIndexPrev; private long mIndexNext; // CONSTRUCTORS /** * Constructor * @param index Which chunk of data should occupy in file * @param readFromFile Does this entry already exist in the file? * @throws StorageFileException */ private SessionKeys(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); } byte flags = dataPlain[OFFSET_FLAGS]; boolean keysSent = ((flags & (1 << 7)) == 0) ? false : true; boolean keysConfirmed = ((flags & (1 << 6)) == 0) ? false : true; boolean simSerial = ((flags & (1 << 5)) == 0) ? false : true; setKeysSent(keysSent); setKeysConfirmed(keysConfirmed); setSimNumber(new SimNumber(Charset.fromAscii8(dataPlain, OFFSET_SIMNUMBER, LENGTH_SIMNUMBER), simSerial)); setSessionKey_Out(LowLevel.cutData(dataPlain, OFFSET_SESSIONKEY_OUTGOING, LENGTH_SESSIONKEY)); setSessionKey_In(LowLevel.cutData(dataPlain, OFFSET_SESSIONKEY_INCOMING, LENGTH_SESSIONKEY)); setPrivateKey(LowLevel.cutData(dataPlain, OFFSET_PRIVATE_KEY, LENGTH_PRIVATE_KEY)); setTimeStamp(LowLevel.getLong(LowLevel.cutData(dataPlain, OFFSET_TIMESTAMP, LENGTH_TIMESTAMP))); setIndexParent(LowLevel.getUnsignedInt(dataPlain, OFFSET_PARENTINDEX)); setIndexPrev(LowLevel.getUnsignedInt(dataPlain, OFFSET_PREVINDEX)); setIndexNext(LowLevel.getUnsignedInt(dataPlain, OFFSET_NEXTINDEX)); } else { // default values setKeysSent(false); setKeysConfirmed(false); setSimNumber(new SimNumber()); setSessionKey_Out(Encryption.getEncryption().generateRandomData(LENGTH_SESSIONKEY)); setSessionKey_In(Encryption.getEncryption().generateRandomData(LENGTH_SESSIONKEY)); setPrivateKey(Encryption.getEncryption().generateRandomData(LENGTH_PRIVATE_KEY)); setTimeStamp(0L); setIndexParent(0L); setIndexPrev(0L); setIndexNext(0L); saveToFile(); } synchronized (cacheSessionKeys) { cacheSessionKeys.add(this); } } // FUNCTIONS /** * Saves contents of the class to the storage file. * * @throws StorageFileException the storage file exception */ public void saveToFile() throws StorageFileException { byte[] keysBuffer = new byte[Storage.ENCRYPTED_ENTRY_SIZE]; // flags byte flags = 0; if (this.mKeysSent) flags |= (byte) ((1 << 7) & 0xFF); if (this.mKeysConfirmed) flags |= (byte) ((1 << 6) & 0xFF); if (this.mSimNumber.isSerial()) flags |= (byte) ((1 << 5) & 0xFF); keysBuffer[OFFSET_FLAGS] = flags; // phone number System.arraycopy(Charset.toAscii8(this.mSimNumber.getNumber(), LENGTH_SIMNUMBER), 0, keysBuffer, OFFSET_SIMNUMBER, LENGTH_SIMNUMBER); // session keys and last IDs and confirmation nonce System.arraycopy(this.mSessionKey_Out, 0, keysBuffer, OFFSET_SESSIONKEY_OUTGOING, LENGTH_SESSIONKEY); System.arraycopy(this.mSessionKey_In, 0, keysBuffer, OFFSET_SESSIONKEY_INCOMING, LENGTH_SESSIONKEY); System.arraycopy(this.mPrivateKey, 0, keysBuffer, OFFSET_PRIVATE_KEY, LENGTH_PRIVATE_KEY); System.arraycopy(LowLevel.getBytesLong(this.mTimeStamp), 0, keysBuffer, OFFSET_TIMESTAMP, LENGTH_TIMESTAMP); // random data System.arraycopy(Encryption.getEncryption().generateRandomData(LENGTH_RANDOMDATA), 0, keysBuffer, OFFSET_RANDOMDATA, LENGTH_RANDOMDATA); // indices System.arraycopy(LowLevel.getBytesUnsignedInt(this.mIndexParent), 0, keysBuffer, OFFSET_PARENTINDEX, 4); System.arraycopy(LowLevel.getBytesUnsignedInt(this.mIndexPrev), 0, keysBuffer, OFFSET_PREVINDEX, 4); System.arraycopy(LowLevel.getBytesUnsignedInt(this.mIndexNext), 0, keysBuffer, OFFSET_NEXTINDEX, 4); // encrypt and save byte[] dataEncrypted = null; try { dataEncrypted = Encryption.getEncryption().encryptSymmetricWithMasterKey(keysBuffer); } catch (EncryptionException e) { throw new StorageFileException(e); } Storage.getStorage().setEntry(mEntryIndex, dataEncrypted); } /** * Returns an instance of the Conversation class that is the parent of this SessionKeys in the data structure * @return * @throws StorageFileException */ public Conversation getParent() throws StorageFileException { if (mIndexParent == 0) return null; return Conversation.getConversation(mIndexParent); } /** * Returns an instance of the predecessor in the list of session keys for parent conversation * @return * @throws StorageFileException */ public SessionKeys getPreviousSessionKeys() throws StorageFileException { if (mIndexPrev == 0) return null; return getSessionKeys(mIndexPrev); } /** * Returns an instance of the successor in the list of session keys for parent conversation * @return * @throws StorageFileException */ public SessionKeys getNextSessionKeys() throws StorageFileException { if (mIndexNext == 0) return null; return getSessionKeys(mIndexNext); } /** * Delete Message and all the MessageParts it controls. * * @throws StorageFileException the storage file exception */ public void delete() throws StorageFileException { SessionKeys prev = this.getPreviousSessionKeys(); SessionKeys next = this.getNextSessionKeys(); if (prev != null) { // this is not the first message in the list // update the previous one prev.setIndexNext(this.getIndexNext()); prev.saveToFile(); } else { // this IS the first message in the list // update parent Conversation parent = this.getParent(); parent.setIndexSessionKeys(this.getIndexNext()); parent.saveToFile(); } // update next one if (next != null) { next.setIndexPrev(this.getIndexPrev()); next.saveToFile(); } // delete this message Empty.replaceWithEmpty(mEntryIndex); // remove from cache synchronized (cacheSessionKeys) { cacheSessionKeys.remove(this); } // make this instance invalid this.mEntryIndex = -1L; } public enum SessionKeysStatus { SENDING_KEYS, SENDING_CONFIRMATION, WAITING_FOR_REPLY, KEYS_EXCHANGED, KEYS_EXPIRED } /** * Returns the status of session keys exchange * @param simNumber * @return * @throws StorageFileException * @throws IOException */ public SessionKeysStatus getStatus() { if (mKeysSent) { if (mKeysConfirmed) if (System.currentTimeMillis() - mTimeStamp > KEYS_VALID_FOR) return SessionKeysStatus.KEYS_EXPIRED; else return SessionKeysStatus.KEYS_EXCHANGED; else return SessionKeysStatus.WAITING_FOR_REPLY; } else { if (mKeysConfirmed) return SessionKeysStatus.SENDING_CONFIRMATION; else return SessionKeysStatus.SENDING_KEYS; } } /** * Increments outgoing session key * * @param count the count */ public void incrementOut(int count) { for (int i = 0; i < count; ++i) setSessionKey_Out(Encryption.getEncryption().getHash(getSessionKey_Out())); } /** * Increments incoming session key * * @param count the count */ public void incrementIn(int count) { for (int i = 0; i < count; ++i) setSessionKey_In(Encryption.getEncryption().getHash(getSessionKey_In())); } // GETTERS / SETTERS long getEntryIndex() { return mEntryIndex; } public boolean getKeysSent() { return mKeysSent; } public void setKeysSent(boolean keysSent) { mKeysSent = keysSent; } public boolean getKeysConfirmed() { return mKeysConfirmed; } public void setKeysConfirmed(boolean keysConfirmed) { mKeysConfirmed = keysConfirmed; } public SimNumber getSimNumber() { return mSimNumber; } public void setSimNumber(SimNumber simNumber) { this.mSimNumber = simNumber; } public byte[] getSessionKey_Out() { return mSessionKey_Out; } public void setSessionKey_Out(byte[] sessionKeyOut) { mSessionKey_Out = sessionKeyOut; } public byte[] getSessionKey_In() { return mSessionKey_In; } public void setSessionKey_In(byte[] sessionKeyIn) { mSessionKey_In = sessionKeyIn; } public byte[] getPrivateKey() { return mPrivateKey; } public void setPrivateKey(byte[] privateKey) { mPrivateKey = privateKey; } public long getTimeStamp() { return mTimeStamp; } public void setTimeStamp(long timeStamp) { mTimeStamp = timeStamp; } 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; } void setIndexParent(long indexParent) { this.mIndexParent = indexParent; } long getIndexParent() { return mIndexParent; } }