FICUSONLINE F9E
X3DH公開鍵サーバ
X3DHによるユーザー間の鍵交換プロセスに必要な公開鍵(Identity Key、Signed PreKey、One-time PreKey)を登録するサーバを、Nginx、MariaDB、PHPを用いて構築します。各鍵の登録や取得の際には、アカウントマネージャのユーザ認証を利用します。
Takanobu FuseAdministrator

3 months ago

Cloud / Server

LIMEリファレンス

下記内容についての質問・要望点などあれば下記アドレス宛送信願います。

[email protected]

以下のLIMEリファレンスガイドから、各鍵の仕様・鍵生成フロー(X3DH、ダブルラチェット)などを確認します。

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

Nodejsテストサーバは、HTTPSリクエスト処理の参考として使用します。

Nodejsテストサーバ

LIME Key Server

E2E Message

E2E Security Auth

Linphone Desktop Message Encryption


X3DHメッセージ

LinphoneからHTTPSリクエストされるX3DHメッセージの構成パケット

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

データベーステーブルの作成

以下のPHPスクリプトに含まれるSQLスクリプトを使用して、Users、OPk、Configの3つのテーブルを作成します。

/* 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);");

ユーザ認証プロセス

ユーザ認証プロセスは、ファイル転送サーバ の認証プロセスを参考に作成。

LinphoneのX3DH(LIME)キーサーバURL。設定ファイルの読込と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();
}

?>

使用する楕円曲線をCurve25519とし、ダイジェスト認証の条件設定(TLSクライアント証明書による認証には、Linphoneのソースコード側でも対応する必要あり)

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;
	}
}

ユーザ認証に問題がなければ、X3DHキーサーバへアクセスし、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);
		}
	}
}

// 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();
}
?>