Commit bebbe5cb authored by johan's avatar johan
Browse files

Port lime basic test to java

parent 3f4f1b00
......@@ -65,7 +65,8 @@ namespace lime {
*/
struct RecipientData {
const std::string deviceId; /**< input: recipient deviceId (shall be GRUU) */
lime::PeerDeviceStatus peerStatus; /**< output: after encrypt calls back, it will hold the status of this peer device:\n
lime::PeerDeviceStatus peerStatus; /**< input: if set to fail, this entry will be ignored by the encrypt function\n
output: after encrypt calls back, it will hold the status of this peer device:\n
- unknown: first interaction with this device)
- untrusted: device is kown but we never confirmed its identity public key
- trusted: we already confirmed this device identity public key
......
......@@ -28,7 +28,7 @@ public enum LimeCallbackReturn {
private int native_val; /* Store the native(used by jni) integer value */
LimeCallbackReturn(int val) {
private LimeCallbackReturn(int val) {
native_val = val;
}
......
......@@ -30,9 +30,9 @@ public enum LimeEncryptionPolicy {
* @brief get the native value (used to give input parameter values)
* @return the native value associated
*/
public int getNative() {return native_val;}
protected int getNative() {return native_val;}
LimeEncryptionPolicy(int val) {
private LimeEncryptionPolicy(int val) {
native_val = val;
}
}
......
......@@ -18,11 +18,20 @@
*/
package org.linphone.lime;
/** @brief The encrypt function input/output data structure
*
* give a recipient GRUU and get it back with the header which must be sent to recipient with the cipher text
*/
public class RecipientData {
public String deviceId;
private int peerStatus; // peer Status is stored in native enumeration, so native code can access it easily
public byte[] DRmessage;
public String deviceId; /**< The recipient device id(shall be its GRUU)*/
private int peerStatus; /**< peer Status is stored in native enumeration, so native code can access it easily, it is thus access through methods translating to LimePeerDeviceStatus java enum\n
input: if set to FAIL, this entry will be ignored by the encrypt function\n
output: after encrypt calls back, it will hold the status of this peer device:\n
- UNKNOWN: first interaction with this device)
- UNTRUSTED: device is kown but we never confirmed its identity public key
- TRUSTED: we already confirmed this device identity public key
- FAIL: we could not encrypt for this device, probably because it never published its keys on the X3DH server */
public byte[] DRmessage; /**< after encrypt calls back, it will hold the Double Ratchet message targeted to the specified recipient. */
public LimePeerDeviceStatus getPeerStatus() {
return LimePeerDeviceStatus.fromNative(peerStatus);
......
......@@ -23,7 +23,9 @@ find_package(Java REQUIRED)
include(UseJava)
set (LIMETESTER_SOURCE_FILES_JAVA
org/linphone/limeTester/LimeTesterUtils.java
org/linphone/limeTester/HelloWorld.java
org/linphone/limeTester/LimeLimeTester.java
org/linphone/limeTester/LimeTester.java
)
......
/*
HelloWorld.java
@author Johan Pascal
@copyright Copyright (C) 2019 Belledonne Communications SARL
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.limeTester;
import org.linphone.lime.*;
import javax.net.ssl.*;
import java.util.UUID;
import java.security.cert.X509Certificate;
import java.io.File;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import java.io.DataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.concurrent.*;
/**
* @brief For testing purpose
* Simulate a network transmission, is actually just two static buffer used to
......@@ -39,7 +49,7 @@ class mailBox {
* @brief For testing purpose
* This callback store reference to the input/ouput buffers recipients and cipherMessage
* passed to the encrypt function.
* The callback function is called when encryption is completed. It then post the
* The callback function is called when encryption is completed.
*/
class LimeStatusCallbackImpl_Mailbox implements LimeStatusCallback {
public int success;
......@@ -105,12 +115,10 @@ class LimeStatusCallbackImpl_Mailbox implements LimeStatusCallback {
public class HelloWorld {
/**
/*
* Test Scenario:
* - Create Alice and Bob users
* - Alice encrypts to Bob who decrypts and check it matches the original
* - Bob encryps to Alice who decrypts and check it matches the original
* - Alice and Bob call the update function
*/
public static void hello_world(LimeCurveId curveId, String dbBasename, String x3dhServerUrl, LimePostToX3DH postObj) {
int expected_success = 0;
......@@ -254,5 +262,11 @@ public class HelloWorld {
bobManager.nativeDestructor();
aliceManager = null;
bobManager = null;
// Remove database files
file = new File(aliceDbFilename);
file.delete();
file = new File(bobDbFilename);
file.delete();
}
}
/*
LimeLimeTester.java
@author Johan Pascal
@copyright Copyright (C) 2019 Belledonne Communications SARL
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.limeTester;
import org.linphone.lime.*;
import java.util.UUID;
import java.io.File;
public class LimeLimeTester {
/* Test Scenario:
* - Alice and Bob(d1 and d2) register themselves on X3DH server
* - Alice send message to Bob (d1 and d2)
* - Alice send another message to Bob (d1 and d2)
* - Bob d1 respond to Alice(with bob d2 in copy)
* - Bob d2 respond to Alice(with bob d1 in copy)
* - Alice send another message to Bob(d1 and d2)
* - Delete Alice and Bob devices to leave distant server base clean
*
* At each message check that the X3DH init is present or not in the DR header
* Note: no asynchronous operation will start before the previous is over(callback returns)
*/
public static void basic(LimeCurveId curveId, String dbBasename, String x3dhServerUrl, LimePostToX3DH postObj) {
int expected_success = 0;
int expected_fail = 0;
LimeTesterUtils TesterUtils = new LimeTesterUtils();
// Create a callback, this one will be used for all operations
// which does not produce encrypted output(all except encryptions)
LimeStatusCallbackImpl statusCallback = new LimeStatusCallbackImpl();
// Create db filenames and delete potential existing ones
String curveIdString;
if (curveId == LimeCurveId.C25519) {
curveIdString = ".C25519";
} else {
curveIdString = ".C448";
}
String aliceDbFilename = "alice."+dbBasename+curveIdString+".sqlite3";
String bobDbFilename = "bob."+dbBasename+curveIdString+".sqlite3";
File file = new File(aliceDbFilename);
file.delete();
file = new File(bobDbFilename);
file.delete();
// Create bob and alice lime managers
LimeManager aliceManager = new LimeManager(aliceDbFilename, postObj);
LimeManager bobManager = new LimeManager(bobDbFilename, postObj);
// Create random device id for alice and bob
String AliceDeviceId = "alice."+UUID.randomUUID().toString();
String BobDeviceId1 = "bob.d1."+UUID.randomUUID().toString();
String BobDeviceId2 = "bob.d2."+UUID.randomUUID().toString();
aliceManager.create_user(AliceDeviceId, x3dhServerUrl, curveId, 10, statusCallback);
expected_success+= 1;
assert (statusCallback.wait_for_success(expected_success));
bobManager.create_user(BobDeviceId1, x3dhServerUrl, curveId, 10, statusCallback);
expected_success+= 1;
assert (statusCallback.wait_for_success(expected_success)); // We cannot create both bob's device at the same time as they use the same database
bobManager.create_user(BobDeviceId2, x3dhServerUrl, curveId, 10, statusCallback);
expected_success+= 1;
assert (statusCallback.wait_for_success(expected_success));
assert (statusCallback.fail == expected_fail);
// Alice sends a message to bob
RecipientData[] recipients = new RecipientData[2]; // Bob has 2 devices
recipients[0] = new RecipientData(BobDeviceId1);
recipients[1] = new RecipientData(BobDeviceId2);
LimeOutputBuffer cipherMessage = new LimeOutputBuffer();
aliceManager.encrypt(AliceDeviceId, "bob", recipients, LimeTesterUtils.patterns[0].getBytes(), cipherMessage, statusCallback);
expected_success+= 1;
assert (statusCallback.wait_for_success(expected_success));
// loop on outputs and decrypt with bob Manager
for (RecipientData recipient : recipients) {
LimeOutputBuffer decodedMessage = new LimeOutputBuffer();
assert(TesterUtils.DR_message_holdsX3DHInit(recipient.DRmessage).holdsX3DHInit); // new sessions created, they must convey X3DH init message
assert(bobManager.decrypt(recipient.deviceId, "bob", AliceDeviceId, recipient.DRmessage, cipherMessage.buffer, decodedMessage) != LimePeerDeviceStatus.FAIL);
String s = new String(decodedMessage.buffer);
assert (s.equals(LimeTesterUtils.patterns[0])):"Decoded message is not the encoded one";
}
// encrypt another one, same recipients(we shall have no new X3DH session but still the X3DH init message)
recipients = new RecipientData[2]; // Bob has 2 devices
recipients[0] = new RecipientData(BobDeviceId1);
recipients[1] = new RecipientData(BobDeviceId2);
cipherMessage = new LimeOutputBuffer();
aliceManager.encrypt(AliceDeviceId, "bob", recipients, LimeTesterUtils.patterns[1].getBytes(), cipherMessage, statusCallback);
expected_success+= 1;
assert (statusCallback.wait_for_success(expected_success));
// loop on outputs and decrypt with bob Manager
for (RecipientData recipient : recipients) {
LimeOutputBuffer decodedMessage = new LimeOutputBuffer();
assert(TesterUtils.DR_message_holdsX3DHInit(recipient.DRmessage).holdsX3DHInit); // they must again convey X3DH init message
assert(bobManager.decrypt(recipient.deviceId, "bob", AliceDeviceId, recipient.DRmessage, cipherMessage.buffer, decodedMessage) == LimePeerDeviceStatus.UNTRUSTED); // now we know alice
String s = new String(decodedMessage.buffer);
assert (s.equals(LimeTesterUtils.patterns[1])):"Decoded message is not the encoded one";
}
// bob.d1 reply to alice and copy bob.d2
recipients = new RecipientData[2];
recipients[0] = new RecipientData(AliceDeviceId);
recipients[1] = new RecipientData(BobDeviceId2);
cipherMessage = new LimeOutputBuffer();
bobManager.encrypt(BobDeviceId1, "alice", recipients, LimeTesterUtils.patterns[2].getBytes(), cipherMessage, statusCallback);
expected_success+= 1;
assert (statusCallback.wait_for_success(expected_success));
// decrypt it
LimeOutputBuffer decodedMessage = new LimeOutputBuffer();
assert(TesterUtils.DR_message_holdsX3DHInit(recipients[0].DRmessage).holdsX3DHInit == false); // alice.d1 to bob.d1 already set up the DR Session, we shall not have any X3DH message here
assert(aliceManager.decrypt(recipients[0].deviceId, "alice", BobDeviceId1, recipients[0].DRmessage, cipherMessage.buffer, decodedMessage) == LimePeerDeviceStatus.UNTRUSTED); // we know alice
String s = new String(decodedMessage.buffer);
assert (s.equals(LimeTesterUtils.patterns[2])):"Decoded message is not the encoded one";
decodedMessage = new LimeOutputBuffer();
assert(TesterUtils.DR_message_holdsX3DHInit(recipients[1].DRmessage).holdsX3DHInit); // bob.d1 to bob.d2 is a new session, we must have a X3DH message here
assert(bobManager.decrypt(recipients[1].deviceId, "alice", BobDeviceId1, recipients[1].DRmessage, cipherMessage.buffer, decodedMessage) != LimePeerDeviceStatus.FAIL); // as we use the same manager for bob.d1 and bob.d2, the are untrusted but it shall be unknown, just check it does not fail
s = new String(decodedMessage.buffer);
assert (s.equals(LimeTesterUtils.patterns[2])):"Decoded message is not the encoded one";
// Now do bob.d2 to alice and bob.d1 every one has an open session towards everyone
recipients = new RecipientData[2];
recipients[0] = new RecipientData(AliceDeviceId);
recipients[1] = new RecipientData(BobDeviceId1);
cipherMessage = new LimeOutputBuffer();
bobManager.encrypt(BobDeviceId2, "alice", recipients, LimeTesterUtils.patterns[3].getBytes(), cipherMessage, statusCallback);
expected_success+= 1;
assert (statusCallback.wait_for_success(expected_success));
// decrypt it
decodedMessage = new LimeOutputBuffer();
assert(TesterUtils.DR_message_holdsX3DHInit(recipients[0].DRmessage).holdsX3DHInit == false); // alice.d1 to bob.d1 already set up the DR Session, we shall not have any X3DH message here
assert(aliceManager.decrypt(recipients[0].deviceId, "alice", BobDeviceId2, recipients[0].DRmessage, cipherMessage.buffer, decodedMessage) == LimePeerDeviceStatus.UNTRUSTED); // we know alice
s = new String(decodedMessage.buffer);
assert (s.equals(LimeTesterUtils.patterns[3])):"Decoded message is not the encoded one";
decodedMessage = new LimeOutputBuffer();
assert(TesterUtils.DR_message_holdsX3DHInit(recipients[1].DRmessage).holdsX3DHInit == false); // bob.d2 to bob.d1 is already open so no X3DH message here
assert(bobManager.decrypt(recipients[1].deviceId, "alice", BobDeviceId2, recipients[1].DRmessage, cipherMessage.buffer, decodedMessage) == LimePeerDeviceStatus.UNTRUSTED); // Bob's d1 knows bob's d2
s = new String(decodedMessage.buffer);
assert (s.equals(LimeTesterUtils.patterns[3])):"Decoded message is not the encoded one";
// encrypt another one from alice to bob.d1 and .d2, it must not send X3DH init anymore
recipients = new RecipientData[2]; // Bob has 2 devices
recipients[0] = new RecipientData(BobDeviceId1);
recipients[1] = new RecipientData(BobDeviceId2);
cipherMessage = new LimeOutputBuffer();
aliceManager.encrypt(AliceDeviceId, "bob", recipients, LimeTesterUtils.patterns[4].getBytes(), cipherMessage, statusCallback);
expected_success+= 1;
assert (statusCallback.wait_for_success(expected_success));
// loop on outputs and decrypt with bob Manager
for (RecipientData recipient : recipients) {
decodedMessage = new LimeOutputBuffer();
assert(TesterUtils.DR_message_holdsX3DHInit(recipient.DRmessage).holdsX3DHInit == false);
assert(bobManager.decrypt(recipient.deviceId, "bob", AliceDeviceId, recipient.DRmessage, cipherMessage.buffer, decodedMessage) == LimePeerDeviceStatus.UNTRUSTED); // now we know alice
s = new String(decodedMessage.buffer);
assert (s.equals(LimeTesterUtils.patterns[4])):"Decoded message is not the encoded one";
}
// clean db: delete users
expected_success+= 3;
aliceManager.delete_user(AliceDeviceId, statusCallback);
bobManager.delete_user(BobDeviceId1, statusCallback);
bobManager.delete_user(BobDeviceId2, statusCallback);
assert (statusCallback.wait_for_success(expected_success));
assert (statusCallback.fail == expected_fail);
// Do not forget do deallocate the native ressources
aliceManager.nativeDestructor();
bobManager.nativeDestructor();
aliceManager = null;
bobManager = null;
// Remove database files
file = new File(aliceDbFilename);
file.delete();
file = new File(bobDbFilename);
file.delete();
}
}
/*
LimeTester.java
@author Johan Pascal
@copyright Copyright (C) 2019 Belledonne Communications SARL
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.limeTester;
import org.linphone.lime.*;
import javax.net.ssl.*;
import java.util.UUID;
import java.security.cert.X509Certificate;
import java.io.File;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
import java.io.DataOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.concurrent.*;
/**
* @brief For testing purpose, we just count the success and fail received
* This only mandatory method to implement is callback
* Note: in real situation you may want to implements several variants of
* this interface :
* - dedicated to encrypt call would store reference to in/out buffers
* - used on create_user, delete_user, update would only need to process the
* status.
*/
class LimeStatusCallbackImpl implements LimeStatusCallback {
public int success;
public int fail;
public int timeout;
/**
* @brief Function called by native code when asynchronous processing is completed
*
* @param[in] status an integer mapped lime:CallbackReturn, use LimeCallbackReturn.fromNative to turn it into a java enumeration
* @param[in] message a string message giving some details in case of failure
*/
public void callback(int cstatus, String message) {
LimeCallbackReturn status = LimeCallbackReturn.fromNative(cstatus);
if (status == LimeCallbackReturn.SUCCESS) {
success++;
} else { // status is LimeCallbackReturn.FAIL
fail++;
}
}
public void resetStatus() {
success = 0;
fail = 0;
timeout = 4000;
}
public LimeStatusCallbackImpl() {
resetStatus();
}
public boolean wait_for_success(int expected_success) {
try {
int time = 0;
while (time<timeout && success<expected_success) {
time += 25;
Thread.sleep(25);
}
}
catch (InterruptedException e) {
System.out.println("Interrupt exception in wait for success");
return false;
}
if (expected_success == success) {
return true;
} else {
return false;
}
}
}
class LimePostToX3DH_Sync implements LimePostToX3DH {
/** @brief Function call by native side to post a message to an X3DH server.
* The connection is synchronous so this function also collect the answer
* and send it back to the native library using the LimeManager.process_response native method
*/
public void postToX3DHServer(long ptr, String url, String from, byte[] message) {
try {
// connect to the given URL
String local_url = new String(url.toCharArray());
URL obj = new URL(local_url);
HttpsURLConnection con = (HttpsURLConnection) obj.openConnection();
// set request header
con.setRequestMethod("POST");
con.setRequestProperty("User-Agent", "lime");
con.setRequestProperty("Content-type", "x3dh/octet-stream");
con.setRequestProperty("From", from);
// Send post request
con.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(con.getOutputStream());
wr.write(message, 0, message.length);
wr.flush();
wr.close();
// wait for a response
int responseCode = con.getResponseCode();
InputStream in = con.getInputStream();
ByteArrayOutputStream response = new ByteArrayOutputStream( );
byte[] buffer = new byte[256];
int bufferLength;
while ((bufferLength = in.read(buffer)) != -1){
response.write(buffer, 0, bufferLength);
}
in.close();
// call response process native function
LimeManager.process_X3DHresponse(ptr, responseCode, response.toByteArray());
response.close();
}
catch (Exception e) {
System.out.println("Exception during HTTPS connection" + e.getMessage());
}
}
}
class RunnablePostToHttp implements Runnable {
private Thread t;
private long m_ptr;
private String m_url;
private String m_from;
private byte[] m_message;
RunnablePostToHttp( long ptr, String url, String from, byte[] message) {
m_ptr = ptr;
m_url = url;
m_from = from;
m_message = message;
}
public void run() {
try {
// connect to the given URL
String local_url = new String(m_url.toCharArray());
URL obj = new URL(local_url);
HttpsURLConnection con = (HttpsURLConnection) obj.openConnection();
// set request header
con.setRequestMethod("POST");
con.setRequestProperty("User-Agent", "lime");
con.setRequestProperty("Content-type", "x3dh/octet-stream");
con.setRequestProperty("From", m_from);
// Send post request
con.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(con.getOutputStream());
wr.write(m_message, 0, m_message.length);
wr.flush();
wr.close();
// wait for a response
int responseCode = con.getResponseCode();
InputStream in = con.getInputStream();
ByteArrayOutputStream response = new ByteArrayOutputStream( );
byte[] buffer = new byte[256];
int bufferLength;
while ((bufferLength = in.read(buffer)) != -1){
response.write(buffer, 0, bufferLength);
}
in.close();
// call response process native function
LimeManager.process_X3DHresponse(m_ptr, responseCode, response.toByteArray());
response.close();
}
catch (Exception e) {
System.out.println("Thread interrupted : "+ e.getMessage());
}
}
}
class LimePostToX3DH_Async implements LimePostToX3DH {
/** @brief Function call by native side to post a message to an X3DH server.
* The connection is asynchronous : when called the post function starts a new thread
* and makes it request in it, then collect the answer
* and send it back to the native library using the LimeManager.process_response native method
*/
public void postToX3DHServer(long ptr, String url, String from, byte[] message) {
RunnablePostToHttp poster = new RunnablePostToHttp(ptr, url, from, message);
new Thread(poster).start();
}
}
public class LimeTester {
public static void main(String[] args) {
......@@ -227,10 +58,12 @@ public class LimeTester {
}
// End of INSECURE CODE - USED FOR TESTING ONLY - DO NOT USE THIS IN REAL SITUATION
/**
/*
* Hello World test:
* - mostly a code demo. More details in the c++ hello world test, but it is a good start
* Run it synchronous and asynchronous on both curves.
* Mostly a code demo, see HelloWorld.java for details.
* Run it with synchronous and asynchronous X3DH server access(leading to synchronous or asynchronous lime operations).
* Synchronous is just for simple code example, real situation is much more likely to be asynchronous
* HelloWorld code is meant to be run with asynchronous X3DH server access but is anyway able to run synchronous.
*/
LimePostToX3DH_Sync sync_postObj = new LimePostToX3DH_Sync();
HelloWorld.hello_world(LimeCurveId.C25519, "hello_world", "https://localhost:25519", sync_postObj);
......@@ -240,6 +73,11 @@ public class LimeTester {
HelloWorld.hello_world(LimeCurveId.C25519, "hello_world", "https://localhost:25519", async_postObj);
HelloWorld.hello_world(LimeCurveId.C448, "hello_world", "https://localhost:25520", async_postObj);
/*
* Lime Basic test
*/
LimeLimeTester.basic(LimeCurveId.C25519, "lime_basic", "https://localhost:25519", async_postObj);
LimeLimeTester.basic(LimeCurveId.C448, "lime_basic", "https://localhost:25520", async_postObj);