FICUSONLINE F9E
X3DH Public Key Server
To build a server for registering public keys required for the X3DH key exchange process between users (Identity Key, Signed PreKey, One-time PreKey) using Nginx, MariaDB, and PHP, user authentication by an account manager will be used for key registration and retrieval.
Takanobu FuseAdministrator

3 months ago

Cloud / Server

LIME Reference Guide

If you have any issues and inquiries about the below contents, please send me your message to the below address;

[email protected]

Refer to the LIME reference guide to review the specifications of each key and the key generation flow (X3DH, Double Ratchet), etc.

Linphone Instant Message Encryption v2.0 (Lime v2.0)

The Node.js test server will be used as a reference for handling HTTPS requests.

Nodejs Test Server

LIME Key Server

E2E Message

E2E Security Auth

Linphone Desktop Message Encryption


X3DH Message

The packet structure of the X3DH message requested via HTTPS from Linphone

Protocol Version <1byte> || Message Type <1byte> || Curve Id <1byte> || Message


Protocol Version:

0x01


Message Type:

0x01 : deprecated register User : a device registers its Id and Identity key on X3DH server, this message holds the Ik only and shall be supported for retro-compatibility with old clients only.

0x02: delete User : a device deletes its Id and Identity key from X3DH server.

0x03: post Signed Pre-key : a device publishes a Signed Pre-key on X3DH server.

0x04: post One-time Pre-keys : a device publishes a batch of One-time Pre-keys on X3DH server.

0x05: get peers key bundles : a device requests key bundles for a list of peer devices.

0x06: peers key bundles : X3DH server responds to device with the list of requested key bundles.

0x07: get self One-time Pre-keys : ask server for self One-time Pre-keys Ids available.

0x08: self One-time Pre-keys : server response with a count and list of all One-time Pre-keys Ids available.

0x09: register User : a device registers its Id and Identity key, Signed Pre-key and a batch of One-time Pre-keys on X3DH server.

0xFF: error : something went wrong on server side during processing of client message, server respond with details on failure.


Curve Id:

0x01 (curve 25519)

0x02 (curve 448)


Message:

0x01 : deprecated register User

  • EdDSA Identity Key<32, 57bytes>

0x02: delete User

  • none

0x03: post Signed Pre-key

  • ECDH Signed Pre-key<32, 56bytes>
  • ECDH Signed Pre-key Signature<64, 114bytes>
  • Signed Pre-key Id

0x04: post One-time Pre-keys

  • keys Count MSB
  • keys Count LSB
  • One-time Pre-key bundle<36, 60bytes>{keys Count}

with One-time Pre-key bundle:

  • ECDH One-Time Pre-key<32, 56bytes>
  • One-Time Pre-key Id

0x05: get peers key bundles

  • request Count MSB
  • request Count LSB
  • request{request Count}

with request:

  • Device Id size
  • Device Id<variable size>
  • Device Id<variable size>

0x06: peers key bundles

  • bundles Count MSB
  • bundles Count LSB
  • key Bundle{bundles Count}

with key Bundle(if a the device has published keys on the server):

  • Device Id size
  • Device Id<variable size>
  • Device Id<variable size>
  • bundle flag [0x00,0x01]
  • EdDSA Identity Key<32, 57bytes>
  • ECDH Signed Pre-key<32, 56bytes>
  • Signed Pre-key Id
  • ECDH Signed Pre-key Signature<64, 114bytes>
  • ECDH One-Time Pre-key<32, 56bytes>{0,1} only if bundle flag = 0x01
  • One-Time Pre-key Id{0,1} only if bundle flag = 0x01

or key Bundle(if a the device has not published keys on the server):

  • Device Id size
  • Device Id<variable size>
  • Device Id<variable size>
  • bundle flag [0x02]

0x07: get self One-time Pre-keys

  • none

0x08: self One-time Pre-keys

  • OPk Count MSB
  • OPk Count LSB
  • OPk Id<4bytes>{OPk Count}

0x09: register User

  • EdDSA Identity Key<32, 57bytes>
  • ECDH Signed Pre-key<32, 56bytes>
  • ECDH Signed Pre-key Signature<64, 114bytes>
  • Signed Pre-key Id
  • keys Count
  • One-time Pre-key bundle<36, 60bytes>{keys Count}

with One-time Pre-key bundle:

  • ECDH One-Time Pre-key<32, 56bytes>
  • One-Time Pre-key Id

0xFF: error

  • Error Code[0x00-0x08]
  • Optional error message of variable size Null terminated ASCII string

Create Data Base Tables

Use the SQL script included in the following PHP script to create three tables: Users, OPk, and Config.

/* Users table:
	 *  - id as primary key (internal use)
	 *  - a userId(shall be the GRUU)
	 *  - Ik (Identity key - EDDSA public key)
	 *  - SPk(the current signed pre-key - ECDH public key)
	 *  - SPk_sig(current SPk public key signed with Identity key)
	 *  - SPh_id (id for SPk provided and internally used by client) - 4 bytes unsigned integer
	 */

	/* One Time PreKey table:
	 *  - an id as primary key (internal use)
	 *  - the Uid of user owning that key
	 *  - the public key (ECDH public key)
	 *  - OPk_id (id for OPk provided and internally used by client) - 4 bytes unsigned integer
	 */

	/* Config table: holds version and curveId parameters:
	 *  - Name: the parameter name (version or curveId)
	 *  - Value: the parameter value (db version scheme or curveId mapped to an integer)
	 */

	$db->query("CREATE TABLE Users (
			Uid INTEGER NOT NULL AUTO_INCREMENT,
			UserId TEXT NOT NULL,
			Ik BLOB NOT NULL,
			SPk BLOB DEFAULT NULL,
			SPk_sig BLOB DEFAULT NULL,
			SPk_id INTEGER UNSIGNED DEFAULT NULL,
			PRIMARY KEY(Uid));");

	$db->query("CREATE TABLE OPk (
			id INTEGER NOT NULL AUTO_INCREMENT,
			Uid INTEGER NOT NULL,
			OPk BLOB NOT NULL,
			OPk_id INTEGER UNSIGNED NOT NULL,
			PRIMARY KEY(id),
			FOREIGN KEY(Uid) REFERENCES Users(Uid) ON UPDATE CASCADE ON DELETE CASCADE);");

	$db->query("CREATE TABLE Config(
			Name VARCHAR(20),
			Value INTEGER NOT NULL);");

User Authentication Process

The user authentication process is created with reference to the authentication process of the file transfer server.

Linphone’s X3DH (LIME) key server URL. Load the configuration file and select the elliptic curve used for X3DH.

lime-server.php

<?php

// Include the configuration file AFTER the definition of logLevel
// get configuration file path, default is /etc/lime-server/lime-server.conf
// but give a chance to the webserver to configure it

include $_SERVER["x3dh_config_path"]
	?? "/etc/lime-server/lime-server.conf";
	
//$config_file = "/etc/lime-server/lime-server.conf";

if (curveId == CurveId::CURVE25519) {
    include("x3dh-25519.php");
} elseif (curveId == CurveId::CURVE448) {
    include("x3dh-448.php");
} else {
    error_log ("Error: Unknown Curve " . "\n");
	bad_request();
}

?>

Set the elliptic curve to be used as Curve25519, and configure the conditions for digest authentication (authentication using TLS client certificates also needs to be supported on Linphone’s source code side).

x3dh-25519.php

<?php

include("common.php");

// That one shall be set by the user authentication layer
$userId = $_SERVER['HTTP_FROM'];

// first check server settings
x3dh_checkSettings();

// If digest auth is enabled, check it pass - skip it if the user passed a TLS client authentication
//if (defined("DIGEST_AUTH") && DIGEST_AUTH === true && ($_SERVER['SSL_CLIENT_VERIFY'] != 'SUCCESS')) {
if (defined("DIGEST_AUTH") && DIGEST_AUTH === true) {
	if (check_user_authentication() === true) {
		x3dh_process_request($userId);
	}
}

?>

common.php

<?php

include("x3dh.php");

// log level one of (LogLevel::DISABLED, ERROR, WARNING, MESSAGE, DEBUG)
// default to DISABLED (recommended value)
define ("x3dh_logLevel" , LogLevel::DEBUG);
define ("x3dh_logFile", "/var/opt/belledonne-communications/log/lime-server.log"); // make sure to have actual write permission to this file
define ("x3dh_logDomain", "X3DH"); // in case Logs are mixed with other applications ones, format is [time tag] -Domain- message


// avoid date() warnings
// time zone shall be set in php.ini or in lime configuration file if needed
date_default_timezone_set(@date_default_timezone_get());


/*** User authentication ***/
function auth_get_db_conn()
{
	$conn = (USE_PERSISTENT_CONNECTIONS)
		? mysqli_connect('p:' . AUTH_DB_HOST, AUTH_DB_USER, AUTH_DB_PASSWORD, AUTH_DB_NAME)
		: mysqli_connect(AUTH_DB_HOST, AUTH_DB_USER, AUTH_DB_PASSWORD, AUTH_DB_NAME);

	if (!$conn) {
		x3dh_log(LogLevel::ERROR, "Unable to connect to MySQL base " . AUTH_DB_NAME . ".\nDebugging errno: " . mysqli_connect_errno() . "\nDebugging error: " . mysqli_connect_error() . "\n");
	}
	return $conn;
}

// Nonce are one-time usage, in order to avoid storing them in a table
// The nonce is built using:
// - timestamp : nonce is valid for MIN_NONCE_VALIDITY_PERIOD seconds at minimum and twice it at maximum (our goal is one time usage anyway, typical value shall be 10 )
// - secret key : avoid an attacker to be able to generate a valid nonce
function auth_get_valid_nonces()
{
	$time = time();
	$time -= $time % MIN_NONCE_VALIDITY_PERIOD; // our nonce will be valid at leat MIN_NONCE_VALIDITY_PERIOD seconds and max twice it, so floor the timestamp
	return [
		hash_hmac("sha256", $time, AUTH_NONCE_KEY),
		hash_hmac("sha256", $time - MIN_NONCE_VALIDITY_PERIOD, AUTH_NONCE_KEY)
	];
}

function request_authentication($realm = "sip.ficusonline.com", $username = null)
{
	$has_md5 = false;
	$has_sha256 = false;

	if ($username != null) {
		// Get the password/hash from database to include only available password hash in the authenticate header
		$auth_db = auth_get_db_conn();
		$stmt = $auth_db->prepare(AUTH_QUERY);
		if (!$stmt) {
			x3dh_log(LogLevel::ERROR, "Unable to execute " . AUTH_QUERY . ".\nDebugging errno: " . mysqli_connect_errno() . "\nDebugging error: " . mysqli_connect_error() . "\n");
			$has_md5 = true;
			$has_sha256 = true;
		} else {
			$stmt->bind_param('ss', $username, $realm);
			$stmt->execute();

			if ($query_result = $stmt->get_result()) {
				while ($row = $query_result->fetch_assoc()) {
					$algorithm = $row['algorithm'];
					if ($algorithm == 'CLRTXT') {
						x3dh_log(LogLevel::DEBUG, "User  " . $username . " has clear text password in db \n");
						$has_md5 = true;
						$has_sha256 = true;
						break; // with clear text password, we can reconstruct MD5 or SHA256 hash, don't parse anything else from base
					} elseif ($algorithm == 'MD5') {
						x3dh_log(LogLevel::DEBUG, "User  " . $username . " has md5 password in db \n");
						$has_md5 = true;
					} elseif ($algorithm == 'SHA-256') {
						x3dh_log(LogLevel::DEBUG, "User  " . $username . " has sha256 password in db \n");
						$has_sha256 = true;
					} else {
						x3dh_log(LogLevel::WARNING, "User  " . $username . " uses unrecognised hash algorithm " . $algorithm . " to store password in db \n");
					}
				}
			}

			$stmt->close();
		}
		$auth_db->close();
	} else { // we don't have the username authorize both MD5 and SHA256
		$has_md5 = true;
		$has_sha256 = true;
	}

	if (($has_md5 || $has_sha256) == false) {
		x3dh_log(LogLevel::WARNING, "User  " . $username . " not found in db upon request_authentification\n");
		// reply anyway with both hash authorized
		$has_md5 = true;
		$has_sha256 = true;
	}

	header('HTTP/1.1 401 Unauthorized');
	if ($has_md5 == true) {
		header('WWW-Authenticate: Digest realm="' . $realm .
			'",qop="auth",algorithm=MD5,nonce="' . auth_get_valid_nonces()[0] . '",opaque="' . md5($realm) . '"');
	}

	if ($has_sha256 == true) {
		header('WWW-Authenticate: Digest realm="' . $realm .
			'",qop="auth",algorithm=SHA-256,nonce="' . auth_get_valid_nonces()[0] . '",opaque="' . md5($realm) . '"', false);
	}

	exit();
}

function authenticate($auth_digest, $realm = "sip.ficusonline.com")
{
	// Parse the client authentication data
	preg_match_all('@(realm|username|nonce|uri|nc|cnonce|qop|response|opaque|algorithm)=[\'"]?([^\'",]+)@', $auth_digest, $a);
	$data = array_combine($a[1], $a[2]);
	$username = $data['username'];
	//x3dh_log(LogLevel::DEBUG, "Data  " . $data . " User  " . $username . " check here");

	// Is the nonce valid?
	$valid_nonces = auth_get_valid_nonces();
	if (!hash_equals($valid_nonces[0], $data['nonce']) && !hash_equals($valid_nonces[1], $data['nonce'])) {
		x3dh_log(LogLevel::DEBUG, "User  " . $username . " tried to log using invalid nonce");
		return;
	}

	// check that the authenticated URI and server URI match
	if ($data['uri'] != $_SERVER['REQUEST_URI']) {
		x3dh_log(LogLevel::DEBUG, "User  " . $username . " tried to log using unmatching URI in auth and server request");
		return;
	}

	// Check opaque is correct(even if we use a fixed value: md5($realm))
	if (!hash_equals(md5($realm), $data['opaque'])) {
		x3dh_log(LogLevel::DEBUG, "User  " . $username . " tried to log using invalid auth opaque value");
		return;
	}

	// Get the password/hash from database
	$auth_db = auth_get_db_conn();
	$stmt = $auth_db->prepare(AUTH_QUERY);
	if (!$stmt) {
		x3dh_log(LogLevel::ERROR, "Unable to execute " . AUTH_QUERY . ".\nDebugging errno: " . mysqli_connect_errno() . "\nDebugging error: " . mysqli_connect_error() . "\n");
		return;
	}
	$stmt->bind_param('ss', $username, $realm);
	$stmt->execute();

	// Default requested to MD5 if not specified in header
	$requested_algorithm = (array_key_exists('algorithm', $data))
		? $data['algorithm']
		: 'MD5';

	$password = null;
	if ($query_result = $stmt->get_result()) {
		while ($row = $query_result->fetch_assoc()) {
			$password = $row['password'];
			$algorithm = $row['algorithm'];
			if ($algorithm == 'CLRTXT') {
				x3dh_log(LogLevel::DEBUG, "User  " . $username . " using clear text password from db \n");
				break; // with clear text password, we can reconstruct MD5 or SHA256 hash, don't parse anything else from base
			} elseif ($algorithm == $requested_algorithm) {
				x3dh_log(LogLevel::DEBUG, "User  " . $username . " using " . $row['algorithm'] . " password from db \n");
				break; // we found the requested hash in base, don't parse anything else
			} else { // algo used to store the password in base won't allow to reconstruct the one given in the header, keep parsing query result
				$password = null;
				$algorithm = null;
			}
		}
	}

	$stmt->close();
	$auth_db->close();

	if (is_null($password)) {
		x3dh_log(LogLevel::ERROR, "Unable to find password for User: " . $username . " in format " . $requested_algorithm);
		return;
	}


	//select right hash
	switch ($requested_algorithm) {
		case 'MD5':
			$hash_algo = 'md5';
			break;
		case 'SHA-256':
			$hash_algo = 'sha256';
			break;
		default:
			x3dh_log(LogLevel::ERROR, "Unsupported algo " . $requested_algorithm . " for User:" . $username . "\n");
			break;
	}

	$A1 = ($algorithm == 'CLRTXT')
		? hash($hash_algo, $username . ':' . $data['realm'] . ':' . $password)
		: $password;

	$A2 = hash($hash_algo, getenv('REQUEST_METHOD') . ':' . $data['uri']);
	$valid_response = hash($hash_algo, $A1 . ':' . $data['nonce'] . ':' . $data['nc'] . ':' . $data['cnonce'] . ':' . $data['qop'] . ':' . $A2);

	// Compare with the client response
	if (hash_equals($valid_response, $data['response'])) {
		return $username;
	}
}

function bad_request()
{
	http_response_code(400);
	exit();
}

function check_user_authentication()
{
	//if ($_SERVER['SSL_CLIENT_VERIFY'] == 'SUCCESS') {
	//	x3dh_log(LogLevel::DEBUG, "Authentication successful using TLS client certificate from " . $_SERVER['HTTP_FROM']);
	//	return true;
	//}

	//x3dh_log(LogLevel::DEBUG, "HTTP_FROM: " . $_SERVER['HTTP_FROM'] . " SSL_CLIENT_VERIFY :  " . $_SERVER['SSL_CLIENT_VERIFY']);
	x3dh_log(LogLevel::DEBUG, "HTTP_FROM: " . $_SERVER['HTTP_FROM'] . "\n");

	if (defined("DIGEST_AUTH") && DIGEST_AUTH === true) {
		// Check the configuration
		if (!defined("AUTH_NONCE_KEY") || strlen(AUTH_NONCE_KEY) < 12) {
			x3dh_log(LogLevel::ERROR, "Your limeserver is badly configured, please set a random string in AUTH_NONCE_KEY at least 12 characters long");
			http_response_code(500);
			exit();
		}

		$headers = getallheaders();
		// From is the GRUU('sip(s):username@auth_realm;gr=*;) or just a sip:uri(sip(s):username@auth_realm), we need to extract the username from it:
		// from position of : until the first occurence of @
		// pass it through rawurldecode has GRUU may contain escaped characters
		$username_start_pos = strpos($headers['From'], ':') + 1;
		$username_end_pos = strpos($headers['From'], '@');
		$username = rawurldecode(substr($headers['From'], $username_start_pos, $username_end_pos - $username_start_pos));
		$username_end_pos++; // point to the begining of the realm
		if (!defined("AUTH_REALM")) {
			$auth_realm = (strpos($headers['From'], ';') === false)
				? rawurldecode(substr($headers['From'], $username_end_pos)) // From holds a sip:uri
				: rawurldecode(substr($headers['From'], $username_end_pos, strpos($headers['From'], ';') - $username_end_pos)); // From holds a GRUU
		} else {
			$auth_realm = AUTH_REALM;
		}

		// Get authentication header if there is one
		if (!empty($headers['Auth-Digest'])) {
			x3dh_log(LogLevel::DEBUG, "Auth-Digest = " . $headers['Auth-Digest']);
			$authorization = $headers['Auth-Digest'];
		} elseif (!empty($headers['Authorization'])) {
			x3dh_log(LogLevel::DEBUG, "Authorization = " . $headers['Authorization']);
			$authorization = $headers['Authorization'];
		}

		//x3dh_log(LogLevel::DEBUG, "Auth-Digest: " . $headers['Auth-Digest'] . " Authorization :  " . $headers['Authorization']);

		// Authentication
		if (!empty($authorization)) {
			x3dh_log(LogLevel::DEBUG, "There is a digest authentication header for " . $headers['From'] . " (username " . $username . " requesting Auth on realm " . $auth_realm . " )");
			$authenticated_username = authenticate($authorization, $auth_realm);

			if ($authenticated_username != '') {
				x3dh_log(LogLevel::DEBUG, "Authentication successful for " . $headers['From'] . "with username " . $username);
				return true;
			} else {
				x3dh_log(LogLevel::DEBUG, "Authentication failed for " . $headers['From'] . " requesting authorization for user " . $username);
				request_authentication($auth_realm, $username);
			}
		} else {
			x3dh_log(LogLevel::DEBUG, "There is no authentication digest header for " . $headers['From'] . " requesting authorization for user " . $username);
			request_authentication($auth_realm, $username);
		}
	} else {
		// no auth requested
		return true;
	}
}

If there are no issues with user authentication, access the X3DH key server and perform the registration and retrieval of each key required for X3DH.

x3dh.php

<?php

function x3dh_get_db_conn() {
	$db = mysqli_connect(X3DH_DB_HOST, X3DH_DB_USER, X3DH_DB_PASSWORD, X3DH_DB_NAME);
	if (!$db) {
		error_log ("Error: Unable to connect to MySQL.\nDebugging errno: " . mysqli_connect_errno() . "\nDebugging error: " . mysqli_connect_error() . "\n");
		exit;
	}
	return $db;
}


// be sure we do not display any error or it may mess the returned message
ini_set('display_errors', 'Off');

// emulate simple enumeration
abstract class LogLevel {
	const DISABLED = 0;
	const ERROR = 1;
	const WARNING = 2;
	const MESSAGE = 3;
	const DEBUG = 4;
};

function stringErrorLevel($level) {
	switch($level) {
		case LogLevel::DISABLED: return "DISABLED";
		case LogLevel::ERROR: return "ERROR";
		case LogLevel::WARNING: return "WARNING";
		case LogLevel::MESSAGE: return "MESSAGE";
		case LogLevel::DEBUG: return "DEBUG";
		default: return "UNKNOWN";
	}
}

function x3dh_log($level, $message) {
	if (x3dh_logLevel>=$level) {
		if ($level === LogLevel::ERROR) { // in ERROR case, add a backtrace
			file_put_contents(x3dh_logFile, date("Y-m-d h:m:s")." -".x3dh_logDomain."- ".stringErrorLevel($level)." : $message\n".print_r(debug_backtrace(),true), FILE_APPEND);
		} else {
			file_put_contents(x3dh_logFile, date("Y-m-d h:m:s")." -".x3dh_logDomain."- ".stringErrorLevel($level)." : $message\n", FILE_APPEND);
		}
	}
}

/**
 * Constants settings
 */

// Resource abuse protection
// Setting to 0 disable the function
//lime_max_device_per_user does not apply to this test server as it is used to test the lime lib only and device id are not gruub in the tests
// Do not set this value too low, it shall not be lower than the server_low_limit+batch_size used by client - default is 100+25
const lime_max_opk_per_device = 200;

define ('X3DH_protocolVersion', 0x01);
define ('X3DH_headerSize', 3);

// WARNING: value shall be in sync with defines in client code */
// emulate simple enumeration with abstract class

// https://github.com/BelledonneCommunications/lime/blob/5.3.71/src/lime_x3dh_protocol.cpp

abstract class MessageTypes {
	const unset_type = 0x00;
	const deprecated_registerUser = 0x01;
	const deleteUser = 0x02;
	const postSPk = 0x03;
	const postOPks = 0x04;
	const getPeerBundle = 0x05;
	const peerBundle = 0x06;
	const getSelfOPks = 0x07;
	const selfOPks = 0x08;
	const registerUser=0x09;
	const error = 0xff;
};

abstract class ErrorCodes {
	const bad_content_type = 0x00;
	const bad_curve = 0x01;
	const missing_senderId = 0x02;
	const bad_x3dh_protocol_version = 0x03;
	const bad_size = 0x04;
	const user_already_in = 0x05;
	const user_not_found = 0x06;
	const db_error = 0x07;
	const bad_request = 0x08;
	const server_failure = 0x09;
	const resource_limit_reached=0x0a;
	const unknown_error_code=0xfe;
	const unset_error_code=0xff;
};

abstract class KeyBundleFlag {
	const noOPk = 0x00;
	const OPk = 0x01;
	const noBundle = 0x02;
};

// define keys and signature size in bytes based on the curve used
const keySizes = array(
	CurveId::CURVE25519 => array ('X_pub' => 32, 'X_priv' => 32, 'ED_pub' => 32, 'ED_priv' => 32, 'Sig' => 64),
	CurveId::CURVE448 => array ('X_pub' => 56, 'X_priv' => 56, 'ED_pub' => 57, 'ED_priv' => 57, 'Sig' => 114)
);

function returnError($code, $errorMessage) {
	x3dh_log(LogLevel::WARNING, "return an error message code ".$code." : ".$errorMessage);
	$header = pack("C4",X3DH_protocolVersion, MessageTypes::error, curveId, $code); // build the X3DH response header, append the error code
	file_put_contents('php://output', $header.$errorMessage);
	exit;
}

function returnOk($message) {
	x3dh_log(LogLevel::DEBUG, "Ok return a message ".bin2hex($message));
	file_put_contents('php://output', $message);
	exit;
}

// Check server setting:
function x3dh_checkSettings() {
	// curveId must be 25519 or 448
	switch(curveId) {
		// Authorized values
		case CurveId::CURVE25519 :
		case CurveId::CURVE448 :
			break;
		// Any other are invalid
		default:
			x3dh_log(LogLevel::ERROR, "Process X3DH request given incorrect curveId parameter (".curveId.").");
			returnError(ErrorCodes::server_failure, "X3DH server is not correctly configured");
	}

	// log Level, if not set or incorrect, disable log
	switch(x3dh_logLevel) {
		// Authorized values
		case LogLevel::DISABLED :
		case LogLevel::ERROR :
		case LogLevel::WARNING :
		case LogLevel::MESSAGE :
		case LogLevel::DEBUG :
			break;
		// any other default to DISABLED
		default:
			error_log("X3DH log level setting is invalid ".x3dh_logLevel.". Disable X3DH logs");
			define ("x3dh_logLevel", LogLevel::DISABLED);
	}
}

function x3dh_process_request($userId) {
	// Check setting
	// that will end the script if settings are detected to be incorrect
	// Could be commented out after server settings have been checked to be ok
	x3dh_checkSettings();

	$userId = filter_var($userId, FILTER_SANITIZE_URL); // userId is supposed to be a GRUU so it shall pass untouched the sanitize URL action
	if ($userId == '') {
		x3dh_log(LogLevel::ERROR, "X3DH server got a request without proper user Id");
		returnError(ErrorCodes::missing_senderId, "User not correctly identified");
	}

	// set response content type
	header('Content-type: x3dh/octet-stream');

	// retrieve response body from php://input
	$request = file_get_contents('php://input');
	$requestSize = strlen($request);
	x3dh_log(LogLevel::DEBUG, "Incoming request from ".$userId." is :".bin2hex($request));

	// check available length before parsing, we must at least have a header
	if ($requestSize < X3DH_headerSize) {
		returnError(ErrorCodes::bad_size, "Packet is not even holding a header. Size ".$requestSize);
	}

	// parse X3DH header
	$X3DH_header=unpack("CprotocolVersion/CmessageType/CcurveId", $request);

	x3dh_log(LogLevel::DEBUG, "protocol version ".$X3DH_header['protocolVersion']." messageType ".$X3DH_header['messageType']." curveId ".$X3DH_header['curveId']);

	if ($X3DH_header['protocolVersion'] != X3DH_protocolVersion) {
		returnError(ErrorCodes::bad_x3dh_protocol_version, "Server running X3DH procotol version ".X3DH_protocolVersion.". Can't process packet with version ".$X3DH_header['protocolVersion']);
	}

	if ($X3DH_header['curveId'] != curveId) {
		returnError(ErrorCodes::bad_curve, "Server running X3DH procotol using curve ".((curveId==CurveId.CURVE25519)?"25519":"448")+"(id ".curveId."). Can't process serve client using curveId ".$X3DH_header['curveId']);
	}

	// acknowledge message by sending an empty message with same header (modified in case of getPeerBundle and get selfOPks request)
	$returnHeader = substr($request, 0, X3DH_headerSize);

	// open the db connection
	$db = x3dh_get_db_conn();

	switch ($X3DH_header['messageType']) {
		/* Deprecated Register User Identity Key : Identity Key <EDDSA Public key size >*/
		case MessageTypes::deprecated_registerUser:
			x3dh_log(LogLevel::MESSAGE, "Got a depricated registerUser Message from ".$userId);
			$x3dh_expectedSize = keySizes[curveId]['ED_pub'];
			if ($requestSize < X3DH_headerSize + $x3dh_expectedSize) {
				returnError(ErrorCodes::bad_size, "Depricated Register Identity packet is expexted to be ".(X3DH_headerSize+$x3dh_expectedSize)." bytes, but we got $requestSize bytes");
			}
			$Ik = substr($request, X3DH_headerSize, $x3dh_expectedSize);

			// Acquire lock on the user table to avoid 2 users being written at the same time with the same name
			$db->query("LOCK TABLES Users WRITE");
			// Check that this usedId is not already in base
			if (($stmt = $db->prepare("SELECT Uid FROM Users WHERE UserId = ? LIMIT 1")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				$db->query("UNLOCK TABLES");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}
			$stmt->bind_param('s', $userId);
			$stmt->execute();
			$result = $stmt->get_result();
			$stmt->close();
			if ($result->num_rows !== 0) {
				$db->query("UNLOCK TABLES");
				returnError(ErrorCodes::user_already_in, "Can't insert user ".$userId." - is already present in base");
			}
			// Insert User in DB
			x3dh_log(LogLevel::DEBUG, "Insert user $userId with Ik :".bin2hex($Ik));
			if (($stmt = $db->prepare("INSERT INTO Users(UserId,Ik) VALUES(?,?)")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				$db->query("UNLOCK TABLES");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}
			$null = NULL;
			$stmt->bind_param('sb', $userId, $null);
			$stmt->send_long_data(1,$Ik);
			$status = $stmt->execute();
			$stmt->close();
			$db->query("UNLOCK TABLES");

			if (!$status) {
				x3dh_log(LogLevel::ERROR, "User $userId INSERT failed error is ".$db->error);
				returnError(ErrorCodes::db_error, "Error while trying to insert user ".$userId);
			}

			// we're done
			returnOk($returnHeader);
			break;
		
		/* Register User Identity Key :
		* Identity Key <EDDSA Public key size > |
		* Signed Pre Key <ECDH public key size> | SPk Signature <Signature length> | SPk id <uint32_t big endian: 4 bytes>
		* OPk number < uint16_t big endian: 2 bytes> | (OPk <ECDH public key size> | OPk id <uint32_t big endian: 4 bytes> ){OPk number} */
		case MessageTypes::registerUser:
			x3dh_log(LogLevel::MESSAGE, "Got a registerUser Message from ".$userId);
			$x3dh_expectedSize = keySizes[curveId]['ED_pub']
								+ keySizes[curveId]['X_pub'] + keySizes[curveId]['Sig'] + 4
                            	+ 2;
			if ($requestSize < X3DH_headerSize + $x3dh_expectedSize) {
				returnError(ErrorCodes::bad_size, "Register Identity packet is expexted to be ".(X3DH_headerSize+$x3dh_expectedSize)." bytes, but we got $requestSize bytes");
			}


			$bufferIndex = X3DH_headerSize;
			$OPk_number = unpack("n", substr($request, $bufferIndex + $x3dh_expectedSize - 2, 2))[1];

			if (lime_max_opk_per_device > 0 && $OPk_number > lime_max_opk_per_device) {
				returnError('resource_limit_reached',  $userId . " is trying to register itself with ".$OPk_number." OPks but server has a limit of ".lime_max_opk_per_device);
			}

			$x3dh_expectedSize += $OPk_number * (keySizes[curveId]['X_pub'] + 4);
			if ($requestSize < X3DH_headerSize + $x3dh_expectedSize) {
				returnError(ErrorCodes::bad_size, "Register User packet is expected to be (with ".$OPk_number." OPks) ".(X3DH_headerSize + $x3dh_expectedSize)." bytes, but we got ".$requestSize." bytes");
			}

			$Ik = substr($request, $bufferIndex, keySizes[curveId]['ED_pub']);
			$bufferIndex += keySizes[curveId]['ED_pub'];
			$SPk = substr($request, $bufferIndex, keySizes[curveId]['X_pub']);
			$bufferIndex += keySizes[curveId]['X_pub'];
			$Sig = substr($request, $bufferIndex, keySizes[curveId]['Sig']);
			$bufferIndex += keySizes[curveId]['Sig'];
			$SPk_id = unpack("N", substr($request, $bufferIndex, 4))[1];
			$bufferIndex += 6;

			$OPks_param = [];
			for ($i = 0; $i < $OPk_number; $i++) {
				$OPk = substr($request, $bufferIndex, keySizes[curveId]['X_pub']);
				$bufferIndex += keySizes[curveId]['X_pub'];
				$OPk_id = unpack("N", substr($request, $bufferIndex, 4))[1];
				$bufferIndex += 4;
				$OPks_param[] = [$OPk, $OPk_id];
			}
			// Acquire lock on the user table to avoid 2 users being written at the same time with the same name
			$db->query("LOCK TABLES Users WRITE");
			// Check that this usedId is not already in base
			if (($stmt = $db->prepare("SELECT Uid, Ik, SPk, SPk_sig, SPk_id FROM Users WHERE UserId = ? LIMIT 1")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				$db->query("UNLOCK TABLES");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}
			$stmt->bind_param("s", $userId);
			$stmt->execute();
			$result=$stmt->get_result();
			$stmt->close();
			if ($result->num_rows > 0) {
				$row = $result->fetch_assoc();
				// Check if Ik, SPk, SPk_sig match
				if ($Ik == $row['Ik'] && $SPk == $row['SPk'] && $Sig == $row['SPk_sig'] && $SPk_id == $row['SPk_id']) {
					$db->query("UNLOCK TABLES");
					x3dh_log(LogLevel::WARNING, "User ".$userId." was already in db, reinsertion with same Ik and SPk required, do nothing and return Ok");
					returnOk($returnHeader);
				} else {
					$db->query("UNLOCK TABLES");
					returnError(ErrorCodes::user_already_in, "Can't insert user ".$userId." - is already present in base and we try to insert a new one with differents Keys");
				}
			} else {
				// before version 5.6.5, begin_transaction doesn't support READ_WRITE flag
				if ($db->server_version < 50605) {
					$db->begin_transaction();
				} else {
					$db->begin_transaction(MYSQLI_TRANS_START_READ_WRITE);
				}
			
				$stmt_insert = $db->prepare("INSERT INTO Users (UserId, Ik, SPk, SPk_sig, SPk_id) VALUES (?, ?, ?, ?, ?)");
				$stmt_insert->bind_param("sbbbi", $userId, $Ik, $SPk, $Sig, $SPk_id);
				$stmt_insert->send_long_data(1,$Ik);
				$stmt_insert->send_long_data(2,$SPk);
				$stmt_insert->send_long_data(3,$Sig);
				$errInsert = $stmt_insert->execute();
				$stmt_insert->close();
			
				if ($errInsert) {
					$Uid = $db->insert_id;
			
					$stmt_opk = $db->prepare("INSERT INTO OPk (Uid, OPk, OPk_id) VALUES (?, ?, ?)");
			
					foreach ($OPks_param as $param) {
						$stmt_opk->bind_param("ibi", $Uid, $param[0], $param[1]);
						$stmt_opk->send_long_data(1,$param[0]);
						$stmt_err = $stmt_opk->execute();
			
						if (!$stmt_err) {
							$stmt_opk->close();
							$db->rollback();
							$db->query("UNLOCK TABLES");
							returnError(ErrorCodes::db_error, "Error while trying to insert OPk for user" .$userId. " Backend says: ".$db->error);
						}
					}

					$stmt_opk->close();
					$db->commit();
					$db->query("UNLOCK TABLES");
					returnOk($returnHeader);
					
				} else {
					$db->rollback();
					$db->query("UNLOCK TABLES");
					returnError(ErrorCodes::db_error, "Error while trying to insert user" .$userId. " INSERT failed, error is: ".$db->error);
				}
			}
			break;

		/* Delete user: message is empty(or at least shall be, anyway, just ignore anything present in the messange and just delete the user given in From header */
		case MessageTypes::deleteUser:
			x3dh_log(LogLevel::MESSAGE, "Got a deleteUser Message from ".$userId);
			if (($stmt = $db->prepare("DELETE FROM Users WHERE UserId = ?")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}
			$stmt->bind_param('s', $userId);
			$status = $stmt->execute();
			$stmt->close();

			if ($status) {
				returnOk($returnHeader);
			} else {
				x3dh_log(LogLevel::ERROR, "User $userId DELETE failed err is ".$db->error);
				returnError(ErrorCodes::db_error, "Error while trying to delete user ".$userId);
			}
			break;

		/* Post Signed Pre Key: Signed Pre Key <ECDH public key size> | SPk Signature <Signature length> | SPk id <uint32_t big endian: 4 bytes> */
		case MessageTypes::postSPk:
			x3dh_log(LogLevel::MESSAGE, "Got a postSPk Message from ".$userId);

			$x3dh_expectedSize = keySizes[curveId]['X_pub'] + keySizes[curveId]['Sig'] + 4;
			// check message length
			if ($requestSize < X3DH_headerSize + $x3dh_expectedSize) {
				returnError(ErrorCodes::bad_size, "post SPK packet is expexted to be ".(X3DH_headerSize+$x3dh_expectedSize)." bytes, but we got $requestSize bytes");
			}

			// parse message
			$bufferIndex = X3DH_headerSize;
			$SPk = substr($request, $bufferIndex, keySizes[curveId]['X_pub']);
			$bufferIndex += keySizes[curveId]['X_pub'];
			$Sig = substr($request, $bufferIndex, keySizes[curveId]['Sig']);
			$bufferIndex += keySizes[curveId]['Sig'];
			$SPk_id = unpack('N', substr($request, $bufferIndex))[1]; // SPk id is a 32 bits unsigned integer in Big endian

			// check we have a matching user in DB
			if (($stmt = $db->prepare("SELECT Uid FROM Users WHERE UserId = ? LIMIT 1")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}
			$stmt->bind_param('s', $userId);
			$stmt->execute();
			$result = $stmt->get_result();
			if ($result->num_rows === 0) { // user not found
				$stmt->close();
				returnError(ErrorCodes::user_not_found, "Post SPk but ".$userId." not found in db"); // that will exit the script execution
			}

			// get the Uid from previous query
			$row = $result->fetch_assoc();
			$Uid = $row['Uid'];
			$stmt->close();

			// write the SPk
			if (($stmt = $db->prepare("UPDATE Users SET SPk = ?, SPk_sig = ?, SPk_id = ? WHERE Uid = ?")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}
			$null = NULL;
			$stmt->bind_param('bbii', $null, $null, $SPk_id, $Uid);
			$stmt->send_long_data(0,$SPk);
			$stmt->send_long_data(1,$Sig);

			$status = $stmt->execute();
			$stmt->close();

			if ($status) {
				returnOk($returnHeader);
			} else {
				x3dh_log(LogLevel::ERROR, "User's $userId SPk INSERT failed err is ".$db->error);
				returnError(ErrorCodes::db_error, "Error while trying to insert SPK for user $userId. Backend says :".$db->error);
			}
			break;

		/* Post OPks : OPk number < uint16_t big endian: 2 bytes> | (OPk <ECDH public key size> | OPk id <uint32_t big endian: 4 bytes> ){OPk number} */
		case MessageTypes::postOPks:
			x3dh_log(LogLevel::MESSAGE,"Got a postOPks Message from ".$userId);
			// get the OPks number in the first message bytes(unsigned int 16 in big endian)
			$bufferIndex = X3DH_headerSize;
			$OPk_number = unpack('n', substr($request, $bufferIndex))[1];
			$x3dh_expectedSize = 2 + $OPk_number*(keySizes[curveId]['X_pub'] + 4); // expect: OPk count<2bytes> + number of OPks*(public key size + OPk_Id<4 bytes>)
			if ($requestSize < X3DH_headerSize + $x3dh_expectedSize) {
				returnError(ErrorCodes::bad_size, "post OPKs packet is expected to be ".(X3DH_headerSize+$x3dh_expectedSize)." bytes, but we got $requestSize bytes");
			}
			$bufferIndex+=2; // point to the beginning of first key

			x3dh_log(LogLevel::DEBUG, "It contains ".$OPk_number." keys");

			// check we have a matching user in DB
			if (($stmt = $db->prepare("SELECT Uid FROM Users WHERE UserId = ? LIMIT 1")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}
			$stmt->bind_param('s', $userId);
			$stmt->execute();
			$result = $stmt->get_result();
			if ($result->num_rows === 0) {
				$stmt->close();
				returnError(ErrorCodes::user_not_found, "Post SPk but ".$userId." not found in db"); // that will exit the script execution
			}

			// get the Uid from previous query
			$row = $result->fetch_assoc();
			$Uid = $row['Uid'];
			$stmt->close();
			
			// Additional codes
			if (lime_max_opk_per_device > 0) {
				// Check OPK total number
				$stmt = $db->prepare("SELECT COUNT(OPK_id) as OPk_count FROM Users as u INNER JOIN OPk as o ON u.Uid=o.Uid WHERE UserId = ?");
				$stmt->bind_param("s", $userId); 
				$stmt->execute();
				$stmt->bind_result($OPk_count);
				$stmt->fetch();
				$stmt->close();
			
				// Error occured
				if ($db->errno) {
					returnError(ErrorCodes::db_error, "Database error in postOPks by " . $userId . ": " . $db->error);
				}
			
				// Initialise OPk = 0
				if (!isset($OPk_count)) {
					$OPk_count = 0;
				}
			
				// Check OPk number over limited
				if ($OPk_count + $OPk_number > lime_max_opk_per_device) { // in case of too many OPk 
					returnError(ErrorCodes::resource_limit_reached, $userId . " is trying to insert " . $OPk_number . " OPks but server has a limit of " . lime_max_opk_per_device . " and it already holds " . $OPk_count);
				}

			}
			// until here

			if (($stmt = $db->prepare("INSERT INTO OPk(Uid, OPk, OPk_id) VALUES(?,?,?)")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}
			$null = NULL;
			$stmt ->bind_param("ibi", $Uid, $null, $OPk_id);

			// before version 5.6.5, begin_transaction doesn't support READ_WRITE flag
			if ($db->server_version < 50605) {
				$db->begin_transaction();
			} else {
				$db->begin_transaction(MYSQLI_TRANS_START_READ_WRITE);
			}
			for ($i = 0; $i<$OPk_number; $i++) {
				$OPk = substr($request, $bufferIndex, keySizes[curveId]['X_pub']);
				$bufferIndex += keySizes[curveId]['X_pub'];
				$stmt->send_long_data(1,$OPk);
				$OPk_id = unpack('N', substr($request, $bufferIndex))[1]; // OPk id is a 32 bits unsigned integer in Big endian
				$bufferIndex += 4;

				if (!$stmt->execute()) {
					$stmt->close();
					$db->rollback();
					returnError(ErrorCodes::db_error, "Error while trying to insert OPk for user ".$userId.". Backend says :".$db->error);
				}
			}

			$stmt->close();
			$db->commit();

			returnOk($returnHeader);
			break;

		/* peerBundle :	bundle Count < 2 bytes unsigned Big Endian> |
		 *	(   deviceId Size < 2 bytes unsigned Big Endian > | deviceId
		 *	    Flag<1 byte: 0 if no OPK in bundle, 1 if present, 2 no bundle> |
		 *	    Ik <EDDSA Public Key Length> |
		 *	    SPk <ECDH Public Key Length> | SPK id <4 bytes>
		 *	    SPk_sig <Signature Length> |
		 *	    (OPk <ECDH Public Key Length> | OPk id <4 bytes>){0,1 in accordance to flag}
		 *	) { bundle Count}
		 */
		case MessageTypes::getPeerBundle:
			x3dh_log(LogLevel::MESSAGE, "Got a getPeerBundle Message from ".$userId);

			// first parse the message
			if ($requestSize < X3DH_headerSize + 2) { // we must have at least 2 bytes to parse peers counts
				returnError(ErrorCodes::bad_size, "post SPKs packet is expected to be at least ".(X3DH_headerSize+2)." bytes, but we got $requestSize bytes");
			}
			// get the peers number in the first message bytes(unsigned int 16 in big endian)
			$bufferIndex = X3DH_headerSize;
			$peersCount = unpack('n', substr($request, $bufferIndex))[1];
			if ($peersCount === 0) {
				returnError(ErrorCodes::bad_request, "Ask for peer Bundles but no device id given");
			}
			$bufferIndex+=2; // point to the beginning of first key

			// build an array of arrays like [[deviceId], [deviceId], ...]
			$peersBundle = array();
			for ($i=0; $i<$peersCount; $i++) {
				// check we have enought bytes to parse before calling unpack
				if ($requestSize - $bufferIndex<2) {
					returnError(ErrorCodes::bad_request, "Malformed getPeerBundle request: peers count doesn't match the device id list length");
				}
				$idLength = unpack('n', substr($request, $bufferIndex))[1];
				$bufferIndex+=2;
				if ($requestSize - $bufferIndex<$idLength) {
					returnError(ErrorCodes::bad_request, "Malformed getPeerBundle request: device id given size doesn't match the string length");
				}
				$peersBundle[] = array(substr($request, $bufferIndex, $idLength));
				$bufferIndex+=$idLength;
			}

			x3dh_log(LogLevel::DEBUG, "Found request for ".$peersCount." keys bundles");
			x3dh_log(LogLevel::DEBUG, print_r($peersBundle, true));

			//$db->query("LOCK TABLES Users WRITE, OPk WRITE"); // lock the OPk table so we won't give twice a one-time pre-key

			// before version 5.6.5, begin_transaction doesn't support READ_WRITE flag
			if ($db->server_version < 50605) {
				$db->begin_transaction();
			} else {
				$db->begin_transaction(MYSQLI_TRANS_START_READ_WRITE);
			}

			if (($stmt = $db->prepare("SELECT u.Ik as Ik, u.SPk as SPk, u.SPk_id as SPk_id, u.SPk_sig as SPk_sig, o.OPk as OPk, o.OPk_id as OPk_id, o.id as id FROM Users as u LEFT JOIN OPk as o ON u.Uid=o.Uid WHERE UserId = ? AND u.SPk IS NOT NULL LIMIT 1 FOR UPDATE")) === FALSE) {
				x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
				returnError(ErrorCodes::db_error, "Server database not ready");
			}

			$keyBundleErrors = ''; // an empty string to store error message if any
			for ($i=0; $i<$peersCount; $i++) {
				$stmt->bind_param('s', $peersBundle[$i][0]);
				$stmt->execute();
				$row = $stmt->get_result()->fetch_assoc();
				if (!$row) {
					x3dh_log(LogLevel::DEBUG, "User ".$peersBundle[$i][0]." not found");
				} else {
					array_push($peersBundle[$i], $row['Ik'], $row['SPk'], $row['SPk_id'], $row['SPk_sig']);
					if ($row['OPk']) { // there is an OPk
						array_push($peersBundle[$i], $row['OPk'], $row['OPk_id']);
						// now that we used that one, we MUST delete it
						if (($del_stmt = $db->prepare("DELETE FROM OPk WHERE id = ?")) === FALSE) {
							x3dh_log(LogLevel::ERROR, "Database appears to be not what we expect");
							returnError(ErrorCodes::db_error, "Server database not ready");
						}
						$del_stmt->bind_param('i', $row['id']);
						if (!$del_stmt->execute()) {
							$keyBundleErrors .=' ## could not delete OPk key retrieved user '.$peersBundle[$i][0]." err : ".$db->error;
						}
						$del_stmt->close();
					}
				}
			}
			$stmt->close();

			// No errors: commit transition, release lock and build the message out of the key bundles extracted
			if($keyBundleErrors === '') {
				$db->commit();
				
				$peersBundleBuffer = pack("C3n", X3DH_protocolVersion, MessageTypes::peerBundle, curveId, $peersCount);
				for ($i = 0; $i < $peersCount; $i++) {
					$deviceId = $peersBundle[$i][0];
					$haveOPk = (count($peersBundle[$i]) > 5)?(KeyBundleFlag::OPk):(KeyBundleFlag::noOPk);
					$peerBundle = pack("n", strlen($deviceId)); // device Id size on 2 bytes in Big Endian
					if (count($peersBundle[$i]) == 1) { // no key bundle
						$flag = keyBundleFlag::noBundle;
						$peerBundle .= $deviceId . pack("C", $flag);
					} else { // have key bundle
						$flag = $haveOPk ? keyBundleFlag::OPk : keyBundleFlag::noOPk;
						$SPk_id = pack("N", $peersBundle[$i][3]); // SPk id on 4 bytes in Big Endian
						$peerBundle .= $deviceId . pack("C", $flag) . $peersBundle[$i][1] . $peersBundle[$i][2] . $SPk_id . $peersBundle[$i][4];
						if ($haveOPk) {
							$OPk_id = pack("N", $peersBundle[$i][6]); // OPk id on 4 bytes in Big Endian
							$peerBundle .= $peersBundle[$i][5] . $OPk_id;
						}
					}				
					$peersBundleBuffer .= $peerBundle;
				}
				returnOk($peersBundleBuffer);

			} else { // something went wrong
				$db->rollback();
				$db->query("UNLOCK TABLES");
				returnError(ErrorCodes::db_error, "Error while trying to get peers bundles :".$keyBundleErrors);
			}
			break;

		/* selfOPks :	OPKs Id Count < 2 bytes unsigned Big Endian> |
		 *	    (OPk id <4 bytes>){ OPks Id Count}
		 */
		case MessageTypes::getSelfOPks:
			x3dh_log(LogLevel::MESSAGE, "Process a getSelfOPks Message from ".$userId);
			
			$stmt = $db->prepare("SELECT Uid FROM Users WHERE UserId = ? LIMIT 1");
			$stmt->bind_param("s", $userId);
			$stmt->execute();
			$stmt->get_result();
			
			if ($stmt->num_rows === 0) {
				// If no users found 
				$stmt->close();
				returnError(ErrorCodes::user_not_found, "Get Self OPks but ".$userId." not found in db");
			} 
			
			// Get OPk ID for user
			$stmt = $db->prepare("SELECT o.OPk_id as OPk_id FROM Users as u INNER JOIN OPk as o ON u.Uid = o.Uid WHERE UserId = ?");
			$stmt->bind_param("s", $userId);
			$stmt->execute();
			$result = $stmt->get_result();
			$stmt->close();

			$selfOPKsBuffer = pack("C3n", X3DH_protocolVersion, MessageTypes::selfOPks, curveId, $result->num_rows);
	
			while ($row = $result->fetch_assoc()) {
				$selfOPKsBuffer .= pack("N", $row['OPk_id']); // 'N':4 bytes big endian
			}

			returnOk($selfOPKsBuffer);
			break;

		default:
			returnError(ErrorCodes::unknown_error_code, "Unknown message type ".$X3DH_header['messageType']);
			break;
	}

	$db->close();
}
?>