/* * 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 org.spongycastle.crypto.digests.SHA256Digest; import uk.ac.cam.db538.cryptosms.MyApplication; import uk.ac.cam.db538.cryptosms.SimCard; 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.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.SessionKeys.SessionKeysStatus; import uk.ac.cam.db538.cryptosms.storage.StorageFileException; import uk.ac.cam.db538.cryptosms.utils.LowLevel; import uk.ac.cam.db538.cryptosms.utils.SimNumber; /* * Key negotiation message */ public class KeysMessage extends Message { // Set handshake validity period to 5 days (4 - 6 days with different time zones) public static final long HANDSHAKE_VALIDITY_PERIOD = 5L * 24L * 60L * 60L * 1000L; // Clock tolerance is 5 mins public static final long CLOCK_TOLERANCE = 5L * 60L * 1000L; protected static final int OFFSET_DATA = OFFSET_HEADER + LENGTH_HEADER; protected static final int LENGTH_DATA = MessageData.LENGTH_MESSAGE - OFFSET_DATA; public static final int LENGTH_TIMESTAMP = 8; public static final int OFFSET_PUBLIC_KEY = OFFSET_DATA; public static final int OFFSET_TIMESTAMP = OFFSET_PUBLIC_KEY + EllipticCurveDeffieHellman.LENGTH_PUBLIC_KEY; public static final int OFFSET_SIGNATURE = OFFSET_TIMESTAMP + LENGTH_TIMESTAMP; public static final int LENGTH_CONTENT = OFFSET_SIGNATURE + Encryption.ASYM_SIGNATURE_LENGTH; private byte[] mPublicKey; private byte[] mPrivateKey; private long mTimeStamp; private byte[] mOtherPublicKey; private long mOtherTimeStamp; private boolean mIsConfirmation; EllipticCurveDeffieHellman mECDH; /** * Instantiates new key negotiation message (first request) * * @throws StorageFileException the storage file exception */ public KeysMessage() throws StorageFileException { mIsConfirmation = false; mECDH = new EllipticCurveDeffieHellman(); mPublicKey = mECDH.getPublicKey(); mPrivateKey = mECDH.getPrivateKey(); mTimeStamp = System.currentTimeMillis(); } /** * Instantiates new key negotiation message (from reply) * * @param originalTimeStamp time stamp of the first message * @param privateKey private key of this user * @param otherTimeStamp time stamp from the other user's message * @param otherPublicKey public key from the other user's message */ public KeysMessage(long originalTimeStamp, byte[] privateKey, long otherTimeStamp, byte[] otherPublicKey) { mIsConfirmation = false; mECDH = new EllipticCurveDeffieHellman(privateKey); mPublicKey = mECDH.getPublicKey(); mPrivateKey = mECDH.getPrivateKey(); mTimeStamp = originalTimeStamp; mOtherTimeStamp = otherTimeStamp; mOtherPublicKey = otherPublicKey; } /** * Instantiates new key negotiation message (to reply) * * @param otherTimeStamp time stamp from the other user's message * @param otherPublicKey public key from the other user's message * @throws StorageFileException the storage file exception */ public KeysMessage(long otherTimeStamp, byte[] otherPublicKey) throws StorageFileException { mIsConfirmation = true; mECDH = new EllipticCurveDeffieHellman(); mPublicKey = mECDH.getPublicKey(); mPrivateKey = mECDH.getPrivateKey(); mTimeStamp = System.currentTimeMillis(); mOtherTimeStamp = otherTimeStamp; mOtherPublicKey = otherPublicKey; } public byte[] getPublicKey() { return mPublicKey; } public byte[] getPrivateKey() { return mPrivateKey; } public long getTimeStamp() { return mTimeStamp; } private byte[] getKey(String prefix) { return Encryption.getEncryption().getHash( (prefix + mECDH.getSharedKey(mOtherPublicKey).toString()).getBytes() ); } public byte[] getKeyOut() { return getKey(mIsConfirmation ? "0" : "1"); } public byte[] getKeyIn() { return getKey(mIsConfirmation ? "1" : "0"); } public boolean isConfirmation() { return mIsConfirmation; } /** * Returns data ready to be sent via SMS * @return * @throws StorageFileException * @throws MessageException * @throws EncryptionException */ @Override public ArrayList<byte[]> getBytes() throws StorageFileException, MessageException, EncryptionException { SHA256Digest hashing = new SHA256Digest(); if (mIsConfirmation) { hashing.update(getOtherHeader()); hashing.update(LowLevel.getBytesLong(mOtherTimeStamp), 0, 8); hashing.update(mOtherPublicKey, 0, mOtherPublicKey.length); } byte[] timeStampBytes = LowLevel.getBytesLong(mTimeStamp); hashing.update(getHeader()); hashing.update(timeStampBytes, 0, 8); hashing.update(mPublicKey, 0, mPublicKey.length); byte[] hash = new byte[Encryption.HASH_LENGTH]; hashing.doFinal(hash, 0); byte[] signature = Encryption.getEncryption().sign(hash); byte[] data = new byte[OFFSET_DATA + LENGTH_CONTENT]; data[OFFSET_HEADER] = getHeader(); System.arraycopy(mPublicKey, 0, data, OFFSET_PUBLIC_KEY, EllipticCurveDeffieHellman.LENGTH_PUBLIC_KEY); System.arraycopy(timeStampBytes, 0, data, OFFSET_TIMESTAMP, LENGTH_TIMESTAMP); System.arraycopy(signature, 0, data, OFFSET_SIGNATURE, Encryption.ASYM_SIGNATURE_LENGTH); ArrayList<byte[]> dataSms = new ArrayList<byte[]>(1); dataSms.add(data); return dataSms; } /** * Parses the message * * @param idGroup the id group * @return the parses the result */ public static ParseResult parseKeysMessage(ArrayList<Pending> idGroup) { 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); if (idGroup.size() != 1) return new ParseResult(idGroup, PendingParseResult.REDUNDANT_PARTS, null); byte[] dataAll = idGroup.get(0).getData(); String sender = idGroup.get(0).getSender(); byte header = getMessageHeader(dataAll); MessageType type = getMessageType(dataAll); Conversation conv = Conversation.getConversation(sender); SessionKeys keys = null; if (conv != null) keys = conv.getSessionKeys(SimCard.getSingleton().getNumber()); byte[] publicKey = LowLevel.cutData(dataAll, OFFSET_PUBLIC_KEY, EllipticCurveDeffieHellman.LENGTH_PUBLIC_KEY); byte[] timeStampBytes = LowLevel.cutData(dataAll, OFFSET_TIMESTAMP, LENGTH_TIMESTAMP); byte[] signature = LowLevel.cutData(dataAll, OFFSET_SIGNATURE, Encryption.ASYM_SIGNATURE_LENGTH); long timeStamp = LowLevel.getLong(timeStampBytes); // check the time stamp isn't too old or in the future long now = System.currentTimeMillis(); if (timeStamp > now + CLOCK_TOLERANCE) return new ParseResult(idGroup, PendingParseResult.TIMESTAMP_IN_FUTURE, null); else if (now - timeStamp > HANDSHAKE_VALIDITY_PERIOD) return new ParseResult(idGroup, PendingParseResult.TIMESTAMP_OLD, null); // chechk that it isn't a replay if (keys != null && keys.getStatus() == SessionKeysStatus.KEYS_EXCHANGED && timeStamp <= keys.getTimeStamp()) return new ParseResult(idGroup, PendingParseResult.TIMESTAMP_OLD, null); if (type == MessageType.HANDSHAKE) { SHA256Digest hashing = new SHA256Digest(); hashing.update(header); hashing.update(timeStampBytes, 0, 8); hashing.update(publicKey, 0, publicKey.length); byte[] hash = new byte[Encryption.HASH_LENGTH]; hashing.doFinal(hash, 0); // check the signature boolean signatureVerified = false; try { signatureVerified = Encryption.getEncryption().verify(hash, signature, contact.getId()); } catch (EncryptionException e) { } catch (WrongKeyDecryptionException e) { } if (!signatureVerified) return new ParseResult(idGroup, PendingParseResult.COULD_NOT_VERIFY, null); // all seems to be fine, so just retrieve the keys and return the result return new ParseResult(idGroup, PendingParseResult.OK_HANDSHAKE_MESSAGE, new KeysMessage( timeStamp, publicKey )); } else if (type == MessageType.CONFIRM) { // find the session keys for this person if (keys == null || keys.getStatus() != SessionKeysStatus.WAITING_FOR_REPLY) // unexpected return new ParseResult(idGroup, PendingParseResult.COULD_NOT_VERIFY, null); byte[] prevPublicKey = new EllipticCurveDeffieHellman(keys.getPrivateKey()).getPublicKey(); SHA256Digest hashing = new SHA256Digest(); hashing.update(HEADER_HANDSHAKE); hashing.update(LowLevel.getBytesLong(keys.getTimeStamp()), 0, 8); hashing.update(prevPublicKey, 0, prevPublicKey.length); hashing.update(header); hashing.update(timeStampBytes, 0, 8); hashing.update(publicKey, 0, publicKey.length); byte[] hash = new byte[Encryption.HASH_LENGTH]; hashing.doFinal(hash, 0); // check the signature boolean signatureVerified = false; try { signatureVerified = Encryption.getEncryption().verify(hash, signature, contact.getId()); } catch (EncryptionException e) { } catch (WrongKeyDecryptionException e) { } if (!signatureVerified) return new ParseResult(idGroup, PendingParseResult.COULD_NOT_VERIFY, null); // all seems to be fine, so save the result KeysMessage keysMsg = new KeysMessage( keys.getTimeStamp(), keys.getPrivateKey(), timeStamp, publicKey ); keys.setSessionKey_Out(keysMsg.getKeyOut()); keys.setSessionKey_In(keysMsg.getKeyIn()); keys.setKeysConfirmed(true); keys.setPrivateKey(Encryption.getEncryption().generateRandomData(EllipticCurveDeffieHellman.LENGTH_PRIVATE_KEY)); keys.setTimeStamp(timeStamp); keys.saveToFile(); return new ParseResult(idGroup, PendingParseResult.OK_CONFIRM_MESSAGE, keysMsg); } else return new ParseResult(idGroup, PendingParseResult.COULD_NOT_DECRYPT, null); } catch (StorageFileException e) { return new ParseResult(idGroup, PendingParseResult.INTERNAL_ERROR, null); } } private byte getHeader() { if (mIsConfirmation) return HEADER_CONFIRM; else return HEADER_HANDSHAKE; } private byte getOtherHeader() { if (mIsConfirmation) return HEADER_HANDSHAKE; else return HEADER_CONFIRM; } protected static long getMessageTimeStamp(byte[] data) { return LowLevel.getLong(LowLevel.cutData(data, OFFSET_TIMESTAMP, LENGTH_TIMESTAMP)); } @Override protected void onMessageSent(String phoneNumber) throws StorageFileException { Conversation conv = Conversation.getConversation(phoneNumber); if (conv == null) { conv = Conversation.createConversation(); conv.setPhoneNumber(phoneNumber); // will get saved while session keys // are attached } SimNumber simNumber = SimCard.getSingleton().getNumber(); conv.deleteSessionKeys(simNumber); SessionKeys keys = SessionKeys.createSessionKeys(conv); keys.setSimNumber(simNumber); if (mIsConfirmation) { keys.setKeysSent(true); keys.setKeysConfirmed(true); keys.setSessionKey_Out(this.getKeyOut()); keys.setSessionKey_In(this.getKeyIn()); keys.setTimeStamp(this.getTimeStamp()); } else { keys.setKeysSent(true); keys.setKeysConfirmed(false); keys.setPrivateKey(getPrivateKey()); keys.setTimeStamp(getTimeStamp()); keys.saveToFile(); } keys.saveToFile(); } @Override protected void onPartSent(String phoneNumber, int index) throws StorageFileException { } }