/* * 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.Random; import uk.ac.cam.db538.cryptosms.crypto.Encryption; import uk.ac.cam.db538.cryptosms.crypto.EncryptionInterface.EncryptionException; import uk.ac.cam.db538.cryptosms.utils.LowLevel; /** * * Class representing the header entry in the secure storage file. * * @author David Brazdil * */ public class Header { static final int CURRENT_VERSION = 1; private static final int INDEX_HEADER = 0; // FILE FORMAT private static final int LENGTH_PLAIN_HEADER = 4; private static final int LENGTH_RANDOM_STUFF = Encryption.SYM_BLOCK_LENGTH - LENGTH_PLAIN_HEADER; // for alignment private static final int OFFSET_ENCRYPTED_HEADER = LENGTH_PLAIN_HEADER + LENGTH_RANDOM_STUFF; private static final int LENGTH_ENCRYPTED_HEADER = Storage.ENCRYPTED_ENTRY_SIZE - OFFSET_ENCRYPTED_HEADER; private static final int LENGTH_ENCRYPTED_HEADER_WITH_OVERHEAD = LENGTH_ENCRYPTED_HEADER + Encryption.SYM_OVERHEAD; private static final int OFFSET_KEYID = 0; private static final int OFFSET_CONVINDEX = LENGTH_ENCRYPTED_HEADER - 4; private static final int OFFSET_FREEINDEX = OFFSET_CONVINDEX - 4; // CACHING private static Header cacheHeader = null; /** * Removes all instances from the list of cached objects. * Be sure you don't use the instances afterwards. */ public static void forceClearCache() { cacheHeader = null; } /** * Returns an instance of Header class. * @return * @throws StorageFileException */ public static Header getHeader() throws StorageFileException { if (cacheHeader == null) cacheHeader = new Header(true); return cacheHeader; } /** * Only to be called from within Database.createFile() * Forces the header to be created with default values and written to the file. * * @return the header * @throws StorageFileException the storage file exception */ static Header createHeader() throws StorageFileException { cacheHeader = new Header(false); return cacheHeader; } // INTERNAL FIELDS private byte mKeyId; private long mIndexEmpty; private long mIndexConversations; private int mVersion; /** * Constructor * @param readFromFile Does this entry already exist in the file? * @throws StorageFileException */ private Header(boolean readFromDisk) throws StorageFileException { if (readFromDisk) { // read bytes from file byte[] dataAll = Storage.getStorage().getEntry(INDEX_HEADER); // check the first three bytes, looking for SMS in ASCII if (dataAll[0] != (byte) 0x53 || dataAll[1] != (byte) 0x4D || dataAll[2] != (byte) 0x53 ) throw new StorageFileException("Not an SMS history file"); // get the version int version = 0 | (dataAll[3] & 0xFF); // decrypt rest of data byte[] dataEncrypted = new byte[LENGTH_ENCRYPTED_HEADER_WITH_OVERHEAD]; System.arraycopy(dataAll, OFFSET_ENCRYPTED_HEADER, dataEncrypted, 0, LENGTH_ENCRYPTED_HEADER_WITH_OVERHEAD); byte[] dataPlain; try { dataPlain = Encryption.getEncryption().decryptSymmetricWithMasterKey(dataEncrypted); } catch (EncryptionException e) { throw new StorageFileException(e); } // set fields setKeyId(dataPlain[OFFSET_KEYID]); setVersion(version); setIndexEmpty(LowLevel.getUnsignedInt(dataPlain, OFFSET_FREEINDEX)); setIndexConversations(LowLevel.getUnsignedInt(dataPlain, OFFSET_CONVINDEX)); } else { // default values setKeyId((byte) (new Random().nextInt())); setVersion(CURRENT_VERSION); setIndexEmpty(0L); setIndexConversations(0L); saveToFile(); } } /** * Save data to the storage file. * * @throws StorageFileException the storage file exception */ public void saveToFile() throws StorageFileException { ByteBuffer headerBuffer = ByteBuffer.allocate(LENGTH_ENCRYPTED_HEADER); headerBuffer.put(mKeyId); headerBuffer.put(Encryption.getEncryption().generateRandomData(LENGTH_ENCRYPTED_HEADER - 9)); headerBuffer.put(LowLevel.getBytesUnsignedInt(this.getIndexEmpty())); headerBuffer.put(LowLevel.getBytesUnsignedInt(this.getIndexConversations())); ByteBuffer headerBufferEncrypted = ByteBuffer.allocate(Storage.CHUNK_SIZE); headerBufferEncrypted.put((byte) 0x53); // S headerBufferEncrypted.put((byte) 0x4D); // M headerBufferEncrypted.put((byte) 0x53); // S headerBufferEncrypted.put((byte) (this.getVersion() & 0xFF)); // version headerBufferEncrypted.put(Encryption.getEncryption().generateRandomData(LENGTH_RANDOM_STUFF)); // random stuff try { headerBufferEncrypted.put(Encryption.getEncryption().encryptSymmetricWithMasterKey(headerBuffer.array())); } catch (EncryptionException e) { throw new StorageFileException(e); } Storage.getStorage().setEntry(INDEX_HEADER, headerBufferEncrypted.array()); } /** * Return instance of the first object in the empty-entry stack * @return * @throws StorageFileException */ public Empty getFirstEmpty() throws StorageFileException { if (this.mIndexEmpty == 0) return null; else return Empty.getEmpty(this.mIndexEmpty); } /** * Return instance of the first object in the conversations linked list * @return * @throws StorageFileException */ public Conversation getFirstConversation() throws StorageFileException { if (this.mIndexConversations == 0) return null; else return Conversation.getConversation(this.mIndexConversations); } /** * Insert new element into the linked list of conversations. * * @param conv the conv * @throws StorageFileException the storage file exception */ void attachConversation(Conversation conv) throws StorageFileException { long indexFirstInStack = getIndexConversations(); if (indexFirstInStack != 0) { Conversation first = Conversation.getConversation(indexFirstInStack); first.setIndexPrev(conv.getEntryIndex()); first.saveToFile(); } conv.setIndexNext(indexFirstInStack); conv.setIndexPrev(0L); conv.saveToFile(); this.setIndexConversations(conv.getEntryIndex()); this.saveToFile(); } /** * Insert new element into the stack of empty entries. * * @param empty the empty * @throws StorageFileException the storage file exception */ void attachEmpty(Empty empty) throws StorageFileException { long indexFirstInStack = getIndexEmpty(); empty.setIndexNext(indexFirstInStack); empty.saveToFile(); this.setIndexEmpty(empty.getEntryIndex()); this.saveToFile(); } // GETTERS / SETTERS long getIndexEmpty() { return mIndexEmpty; } void setIndexEmpty(long indexEmpty) { if (indexEmpty > 0xFFFFFFFFL || indexEmpty < 0L) throw new IndexOutOfBoundsException(); mIndexEmpty = indexEmpty; } long getIndexConversations() { return mIndexConversations; } void setIndexConversations(long indexConversations) { if (indexConversations > 0xFFFFFFFFL || indexConversations < 0L) throw new IndexOutOfBoundsException(); mIndexConversations = indexConversations; } int getVersion() { return mVersion; } void setVersion(int version) { if (version > 0xFF) throw new IndexOutOfBoundsException(); mVersion = version; } int getKeyId() { return LowLevel.getUnsignedByte(mKeyId); } void setKeyId(int keyId) { mKeyId = LowLevel.getBytesUnsignedByte(keyId); } /** * Increment key id. * * @return the byte */ public byte incrementKeyId() { return mKeyId++; } }