/*
* Copyright 2016 OpenMarket Ltd
* Copyright 2016 Vector Creations Ltd
*
* 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 org.matrix.olm;
import android.content.Context;
import android.support.test.runner.AndroidJUnit4;
import android.text.TextUtils;
import android.util.Log;
import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import static android.support.test.InstrumentationRegistry.getInstrumentation;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class OlmGroupSessionTest {
private static final String LOG_TAG = "OlmSessionTest";
private final String FILE_NAME_SERIAL_OUT_SESSION = "SerialOutGroupSession";
private final String FILE_NAME_SERIAL_IN_SESSION = "SerialInGroupSession";
private static OlmManager mOlmManager;
private static OlmOutboundGroupSession mAliceOutboundGroupSession;
private static String mAliceSessionIdentifier;
private static long mAliceMessageIndex;
private static final String CLEAR_MESSAGE1 = "Hello!";
private static String mAliceToBobMessage;
private static OlmInboundGroupSession mBobInboundGroupSession;
private static String mAliceOutboundSessionKey;
private static String mBobSessionIdentifier;
private static String mBobDecryptedMessage;
@BeforeClass
public static void setUpClass(){
// load native lib
mOlmManager = new OlmManager();
String version = mOlmManager.getOlmLibVersion();
assertNotNull(version);
Log.d(LOG_TAG, "## setUpClass(): lib version="+version);
}
/**
* Basic test:
* - alice creates an outbound group session
* - bob creates an inbound group session with alice's outbound session key
* - alice encrypts a message with its session
* - bob decrypts the encrypted message with its session
* - decrypted message is identical to original alice message
*/
@Test
public void test01CreateOutboundSession() {
// alice creates OUTBOUND GROUP SESSION
try {
mAliceOutboundGroupSession = new OlmOutboundGroupSession();
} catch (OlmException e) {
assertTrue("Exception in OlmOutboundGroupSession, Exception code=" + e.getExceptionCode(), false);
}
}
@Test
public void test02GetOutboundGroupSessionIdentifier() {
// test session ID
mAliceSessionIdentifier = null;
try {
mAliceSessionIdentifier = mAliceOutboundGroupSession.sessionIdentifier();
} catch (Exception e) {
assertTrue(e.getMessage(), false);
}
assertNotNull(mAliceSessionIdentifier);
assertTrue(mAliceSessionIdentifier.length() > 0);
}
@Test
public void test03GetOutboundGroupSessionKey() {
// test session Key
mAliceOutboundSessionKey = null;
try {
mAliceOutboundSessionKey = mAliceOutboundGroupSession.sessionKey();
} catch (Exception e) {
assertTrue(e.getMessage(), false);
}
assertNotNull(mAliceOutboundSessionKey);
assertTrue(mAliceOutboundSessionKey.length() > 0);
}
@Test
public void test04GetOutboundGroupMessageIndex() {
// test message index before any encryption
mAliceMessageIndex = mAliceOutboundGroupSession.messageIndex();
assertTrue(0 == mAliceMessageIndex);
}
@Test
public void test05OutboundGroupEncryptMessage() {
// alice encrypts a message to bob
try {
mAliceToBobMessage = mAliceOutboundGroupSession.encryptMessage(CLEAR_MESSAGE1);
} catch (Exception e) {
assertTrue("Exception in bob encryptMessage, Exception code=" + e.getMessage(), false);
}
assertFalse(TextUtils.isEmpty(mAliceToBobMessage));
// test message index after encryption is incremented
mAliceMessageIndex = mAliceOutboundGroupSession.messageIndex();
assertTrue(1 == mAliceMessageIndex);
}
@Test
public void test06CreateInboundGroupSession() {
// bob creates INBOUND GROUP SESSION with alice outbound key
try {
mBobInboundGroupSession = new OlmInboundGroupSession(mAliceOutboundSessionKey);
} catch (OlmException e) {
assertTrue("Exception in bob OlmInboundGroupSession, Exception code=" + e.getExceptionCode(), false);
}
}
@Test
public void test08GetInboundGroupSessionIdentifier() {
// check both session identifiers are equals
mBobSessionIdentifier = null;
try {
mBobSessionIdentifier = mBobInboundGroupSession.sessionIdentifier();
} catch (Exception e) {
assertTrue(e.getMessage(), false);
}
assertFalse(TextUtils.isEmpty(mBobSessionIdentifier));
}
@Test
public void test09SessionIdentifiersAreIdentical() {
// check both session identifiers are equals: alice vs bob
assertTrue(mAliceSessionIdentifier.equals(mBobSessionIdentifier));
}
@Test
public void test10InboundDecryptMessage() {
mBobDecryptedMessage = null;
OlmInboundGroupSession.DecryptMessageResult result = null;
try {
result = mBobInboundGroupSession.decryptMessage(mAliceToBobMessage);
} catch (Exception e) {
assertTrue(e.getMessage(), false);
}
// test decrypted message
mBobDecryptedMessage = result.mDecryptedMessage;
assertFalse(TextUtils.isEmpty(mBobDecryptedMessage));
assertTrue(0 == result.mIndex);
}
@Test
public void test11InboundDecryptedMessageIdentical() {
// test decrypted message
assertTrue(mBobDecryptedMessage.equals(CLEAR_MESSAGE1));
}
@Test
public void test12ReleaseOutboundSession() {
// release group sessions
mAliceOutboundGroupSession.releaseSession();
}
@Test
public void test13ReleaseInboundSession() {
// release group sessions
mBobInboundGroupSession.releaseSession();
}
@Test
public void test14CheckUnreleaseedCount() {
assertTrue(mAliceOutboundGroupSession.isReleased());
assertTrue(mBobInboundGroupSession.isReleased());
}
@Test
public void test15SerializeOutboundSession() {
OlmOutboundGroupSession outboundGroupSessionRef=null;
OlmOutboundGroupSession outboundGroupSessionSerial;
// create one OUTBOUND GROUP SESSION
try {
outboundGroupSessionRef = new OlmOutboundGroupSession();
} catch (OlmException e) {
assertTrue("Exception in OlmOutboundGroupSession, Exception code=" + e.getExceptionCode(), false);
}
assertNotNull(outboundGroupSessionRef);
// serialize alice session
Context context = getInstrumentation().getContext();
try {
FileOutputStream fileOutput = context.openFileOutput(FILE_NAME_SERIAL_OUT_SESSION, Context.MODE_PRIVATE);
ObjectOutputStream objectOutput = new ObjectOutputStream(fileOutput);
objectOutput.writeObject(outboundGroupSessionRef);
objectOutput.flush();
objectOutput.close();
// deserialize session
FileInputStream fileInput = context.openFileInput(FILE_NAME_SERIAL_OUT_SESSION);
ObjectInputStream objectInput = new ObjectInputStream(fileInput);
outboundGroupSessionSerial = (OlmOutboundGroupSession) objectInput.readObject();
assertNotNull(outboundGroupSessionSerial);
objectInput.close();
// get sessions keys
String sessionKeyRef = outboundGroupSessionRef.sessionKey();
String sessionKeySerial = outboundGroupSessionSerial.sessionKey();
assertFalse(TextUtils.isEmpty(sessionKeyRef));
assertFalse(TextUtils.isEmpty(sessionKeySerial));
// session keys comparison
assertTrue(sessionKeyRef.equals(sessionKeySerial));
// get sessions IDs
String sessionIdRef = outboundGroupSessionRef.sessionIdentifier();
String sessionIdSerial = outboundGroupSessionSerial.sessionIdentifier();
assertFalse(TextUtils.isEmpty(sessionIdRef));
assertFalse(TextUtils.isEmpty(sessionIdSerial));
// session IDs comparison
assertTrue(sessionIdRef.equals(sessionIdSerial));
outboundGroupSessionRef.releaseSession();
outboundGroupSessionSerial.releaseSession();
assertTrue(outboundGroupSessionRef.isReleased());
assertTrue(outboundGroupSessionSerial.isReleased());
} catch (FileNotFoundException e) {
Log.e(LOG_TAG, "## test15SerializeOutboundSession(): Exception FileNotFoundException Msg=="+e.getMessage());
assertTrue(e.getMessage(), false);
} catch (ClassNotFoundException e) {
Log.e(LOG_TAG, "## test15SerializeOutboundSession(): Exception ClassNotFoundException Msg==" + e.getMessage());
assertTrue(e.getMessage(), false);
} catch (OlmException e) {
Log.e(LOG_TAG, "## test15SerializeOutboundSession(): Exception OlmException Msg==" + e.getMessage());
assertTrue(e.getMessage(), false);
} catch (IOException e) {
Log.e(LOG_TAG, "## test15SerializeOutboundSession(): Exception IOException Msg==" + e.getMessage());
assertTrue(e.getMessage(), false);
} catch (Exception e) {
Log.e(LOG_TAG, "## test15SerializeOutboundSession(): Exception Msg==" + e.getMessage());
assertTrue(e.getMessage(), false);
}
}
@Test
public void test16SerializeInboundSession() {
OlmOutboundGroupSession aliceOutboundGroupSession=null;
OlmInboundGroupSession bobInboundGroupSessionRef=null;
OlmInboundGroupSession bobInboundGroupSessionSerial;
// alice creates OUTBOUND GROUP SESSION
try {
aliceOutboundGroupSession = new OlmOutboundGroupSession();
} catch (OlmException e) {
assertTrue("Exception in OlmOutboundGroupSession, Exception code=" + e.getExceptionCode(), false);
}
assertNotNull(aliceOutboundGroupSession);
// get the session key from the outbound group session
String sessionKeyRef = null;
try {
sessionKeyRef = aliceOutboundGroupSession.sessionKey();
} catch (Exception e) {
assertTrue(e.getMessage(), false);
}
assertNotNull(sessionKeyRef);
// bob creates INBOUND GROUP SESSION
try {
bobInboundGroupSessionRef = new OlmInboundGroupSession(sessionKeyRef);
} catch (OlmException e) {
assertTrue("Exception in OlmInboundGroupSession, Exception code=" + e.getExceptionCode(), false);
}
assertNotNull(bobInboundGroupSessionRef);
// serialize alice session
Context context = getInstrumentation().getContext();
try {
FileOutputStream fileOutput = context.openFileOutput(FILE_NAME_SERIAL_IN_SESSION, Context.MODE_PRIVATE);
ObjectOutputStream objectOutput = new ObjectOutputStream(fileOutput);
objectOutput.writeObject(bobInboundGroupSessionRef);
objectOutput.flush();
objectOutput.close();
// deserialize session
FileInputStream fileInput = context.openFileInput(FILE_NAME_SERIAL_IN_SESSION);
ObjectInputStream objectInput = new ObjectInputStream(fileInput);
bobInboundGroupSessionSerial = (OlmInboundGroupSession)objectInput.readObject();
assertNotNull(bobInboundGroupSessionSerial);
objectInput.close();
// get sessions IDs
String aliceSessionId = aliceOutboundGroupSession.sessionIdentifier();
String sessionIdRef = bobInboundGroupSessionRef.sessionIdentifier();
String sessionIdSerial = bobInboundGroupSessionSerial.sessionIdentifier();
assertFalse(TextUtils.isEmpty(aliceSessionId));
assertFalse(TextUtils.isEmpty(sessionIdRef));
assertFalse(TextUtils.isEmpty(sessionIdSerial));
// session IDs comparison
assertTrue(aliceSessionId.equals(sessionIdSerial));
assertTrue(sessionIdRef.equals(sessionIdSerial));
aliceOutboundGroupSession.releaseSession();
bobInboundGroupSessionRef.releaseSession();
bobInboundGroupSessionSerial.releaseSession();
assertTrue(aliceOutboundGroupSession.isReleased());
assertTrue(bobInboundGroupSessionRef.isReleased());
assertTrue(bobInboundGroupSessionSerial.isReleased());
} catch (FileNotFoundException e) {
Log.e(LOG_TAG, "## test16SerializeInboundSession(): Exception FileNotFoundException Msg=="+e.getMessage());
assertTrue(e.getMessage(), false);
} catch (ClassNotFoundException e) {
Log.e(LOG_TAG, "## test16SerializeInboundSession(): Exception ClassNotFoundException Msg==" + e.getMessage());
assertTrue(e.getMessage(), false);
} catch (OlmException e) {
Log.e(LOG_TAG, "## test16SerializeInboundSession(): Exception OlmException Msg==" + e.getMessage());
assertTrue(e.getMessage(), false);
} catch (IOException e) {
Log.e(LOG_TAG, "## test16SerializeInboundSession(): Exception IOException Msg==" + e.getMessage());
assertTrue(e.getMessage(), false);
} catch (Exception e) {
Log.e(LOG_TAG, "## test16SerializeInboundSession(): Exception Msg==" + e.getMessage());
assertTrue(e.getMessage(), false);
}
}
/**
* Create multiple outbound group sessions and check that session Keys are different.
* This test validates random series are provide enough random values.
*/
@Test
public void test17MultipleOutboundSession() {
OlmOutboundGroupSession outboundGroupSession1;
OlmOutboundGroupSession outboundGroupSession2;
OlmOutboundGroupSession outboundGroupSession3;
OlmOutboundGroupSession outboundGroupSession4;
OlmOutboundGroupSession outboundGroupSession5;
OlmOutboundGroupSession outboundGroupSession6;
OlmOutboundGroupSession outboundGroupSession7;
OlmOutboundGroupSession outboundGroupSession8;
try {
outboundGroupSession1 = new OlmOutboundGroupSession();
outboundGroupSession2 = new OlmOutboundGroupSession();
outboundGroupSession3 = new OlmOutboundGroupSession();
outboundGroupSession4 = new OlmOutboundGroupSession();
outboundGroupSession5 = new OlmOutboundGroupSession();
outboundGroupSession6 = new OlmOutboundGroupSession();
outboundGroupSession7 = new OlmOutboundGroupSession();
outboundGroupSession8 = new OlmOutboundGroupSession();
// get the session key from the outbound group sessions
String sessionKey1 = outboundGroupSession1.sessionKey();
String sessionKey2 = outboundGroupSession2.sessionKey();
assertFalse(sessionKey1.equals(sessionKey2));
String sessionKey3 = outboundGroupSession3.sessionKey();
assertFalse(sessionKey2.equals(sessionKey3));
String sessionKey4 = outboundGroupSession4.sessionKey();
assertFalse(sessionKey3.equals(sessionKey4));
String sessionKey5 = outboundGroupSession5.sessionKey();
assertFalse(sessionKey4.equals(sessionKey5));
String sessionKey6 = outboundGroupSession6.sessionKey();
assertFalse(sessionKey5.equals(sessionKey6));
String sessionKey7 = outboundGroupSession7.sessionKey();
assertFalse(sessionKey6.equals(sessionKey7));
String sessionKey8 = outboundGroupSession8.sessionKey();
assertFalse(sessionKey7.equals(sessionKey8));
// get the session IDs from the outbound group sessions
String sessionId1 = outboundGroupSession1.sessionIdentifier();
String sessionId2 = outboundGroupSession2.sessionIdentifier();
assertFalse(sessionId1.equals(sessionId2));
String sessionId3 = outboundGroupSession3.sessionKey();
assertFalse(sessionId2.equals(sessionId3));
String sessionId4 = outboundGroupSession4.sessionKey();
assertFalse(sessionId3.equals(sessionId4));
String sessionId5 = outboundGroupSession5.sessionKey();
assertFalse(sessionId4.equals(sessionId5));
String sessionId6 = outboundGroupSession6.sessionKey();
assertFalse(sessionId5.equals(sessionId6));
String sessionId7 = outboundGroupSession7.sessionKey();
assertFalse(sessionId6.equals(sessionId7));
String sessionId8 = outboundGroupSession8.sessionKey();
assertFalse(sessionId7.equals(sessionId8));
outboundGroupSession1.releaseSession();
outboundGroupSession2.releaseSession();
outboundGroupSession3.releaseSession();
outboundGroupSession4.releaseSession();
outboundGroupSession5.releaseSession();
outboundGroupSession6.releaseSession();
outboundGroupSession7.releaseSession();
outboundGroupSession8.releaseSession();
assertTrue(outboundGroupSession1.isReleased());
assertTrue(outboundGroupSession2.isReleased());
assertTrue(outboundGroupSession3.isReleased());
assertTrue(outboundGroupSession4.isReleased());
assertTrue(outboundGroupSession5.isReleased());
assertTrue(outboundGroupSession6.isReleased());
assertTrue(outboundGroupSession7.isReleased());
assertTrue(outboundGroupSession8.isReleased());
} catch (OlmException e) {
assertTrue("Exception in OlmOutboundGroupSession, Exception code=" + e.getExceptionCode(), false);
}
}
/**
* Specific test for the following run time error:
* "JNI DETECTED ERROR IN APPLICATION: input is not valid Modified UTF-8: illegal start byte 0xf0 in call to NewStringUTF".
* When the msg to decrypt contain emojis, depending on the android platform, the NewStringUTF() behaves differently and
* can even crash.
* This issue is described in details here: https://github.com/eclipsesource/J2V8/issues/142
*/
@Test
public void test18TestBadCharacterCrashInDecrypt() {
OlmInboundGroupSession bobInboundGroupSession=null;
// values taken from a "real life" crash case
String sessionKeyRef = "AgAAAAycZE6AekIctJWYxd2AWLOY15YmxZODm/WkgbpWkyycp6ytSp/R+wo84jRrzBNWmv6ySLTZ9R0EDOk9VI2eZyQ6Efdwyo1mAvrWvTkZl9yALPdkOIVHywyG65f1SNiLrnsln3hgsT1vUrISGyKtsljoUgQpr3JDPEhD0ilAi63QBjhnGCW252b+7nF+43rb6O6lwm93LaVwe2341Gdp6EkhTUvetALezEqDOtKN00wVqAbq0RQAnUJIowxHbMswg+FyoR1K1oCjnVEoF23O9xlAn5g1XtuBZP3moJlR2lwsBA";
String msgToDecryptWithEmoji = "AwgNEpABpjs+tYF+0y8bWtzAgYAC3N55p5cPJEEiGPU1kxIHSY7f2aG5Fj4wmcsXUkhDv0UePj922kgf+Q4dFsPHKq2aVA93n8DJAQ/FRfcM98B9E6sKCZ/PsCF78uBvF12Aaq9D3pUHBopdd7llUfVq29d5y6ZwX5VDoqV2utsATkKjXYV9CbfZuvvBMQ30ZLjEtyUUBJDY9K4FxEFcULytA/IkVnATTG9ERuLF/yB6ukSFR+iUWRYAmtuOuU0k9BvaqezbGqNoK5Grlkes+dYX6/0yUObumcw9/iAI";
// bob creates INBOUND GROUP SESSION
try {
bobInboundGroupSession = new OlmInboundGroupSession(sessionKeyRef);
} catch (OlmException e) {
assertTrue("Exception in test18TestBadCharacterCrashInDecrypt, Exception code=" + e.getExceptionCode(), false);
}
OlmInboundGroupSession.DecryptMessageResult result = null;
try {
result = bobInboundGroupSession.decryptMessage(msgToDecryptWithEmoji);
} catch (Exception e) {
assertTrue("Exception in test18TestBadCharacterCrashInDecrypt, Exception code=" + e.getMessage(), false);
}
assertNotNull(result.mDecryptedMessage);
assertTrue(13 == result.mIndex);
}
/**
* Specific test to check an error message is returned by decryptMessage() API.
* A corrupted encrypted message is passed, and a INVALID_BASE64 is
* espexted.
**/
@Test
public void test19TestErrorMessageReturnedInDecrypt() {
OlmInboundGroupSession bobInboundGroupSession=null;
final String EXPECTED_ERROR_MESSAGE= "INVALID_BASE64";
String sessionKeyRef = "AgAAAAycZE6AekIctJWYxd2AWLOY15YmxZODm/WkgbpWkyycp6ytSp/R+wo84jRrzBNWmv6ySLTZ9R0EDOk9VI2eZyQ6Efdwyo1mAvrWvTkZl9yALPdkOIVHywyG65f1SNiLrnsln3hgsT1vUrISGyKtsljoUgQpr3JDPEhD0ilAi63QBjhnGCW252b+7nF+43rb6O6lwm93LaVwe2341Gdp6EkhTUvetALezEqDOtKN00wVqAbq0RQAnUJIowxHbMswg+FyoR1K1oCjnVEoF23O9xlAn5g1XtuBZP3moJlR2lwsBA";
String corruptedEncryptedMsg = "AwgANYTHINGf87ge45ge7gr*/rg5ganything4gr41rrgr4re55tanythingmcsXUkhDv0UePj922kgf+";
// valid INBOUND GROUP SESSION
try {
bobInboundGroupSession = new OlmInboundGroupSession(sessionKeyRef);
} catch (OlmException e) {
assertTrue("Exception in test19TestErrorMessageReturnedInDecrypt, Exception code=" + e.getExceptionCode(), false);
}
String exceptionMessage = null;
try {
bobInboundGroupSession.decryptMessage(corruptedEncryptedMsg);
} catch (OlmException e) {
exceptionMessage = e.getMessage();
}
assertTrue(0!=EXPECTED_ERROR_MESSAGE.length());
assertTrue(EXPECTED_ERROR_MESSAGE.equals(exceptionMessage));
}
}