lime_localStorage.cpp 36.2 KB
Newer Older
johan's avatar
johan committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
/*
	lime_x3dh.cpp
	Copyright (C) 2017  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/>.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#define BCTBX_LOG_DOMAIN "lime"
#include <bctoolbox/logging.h>

#include "lime/lime.hpp"
#include "lime_localStorage.hpp"
#include "lime_double_ratchet.hpp"
#include "lime_impl.hpp"
#include "bctoolbox/exception.hh"

#include "soci/sqlite3/soci-sqlite3.h"
using namespace::std;
using namespace::soci;
using namespace::lime;

namespace lime {

Db::Db(std::string filename) : sql{sqlite3, filename}{
	int userVersion=-1;
	sql<<"PRAGMA user_version;",into(userVersion);
	sql<<"PRAGMA foreign_keys = ON;"; // make sure this connection enable foreign keys
	if (userVersion != lime::settings::DBuserVersion) {
		if (userVersion > lime::settings::DBuserVersion) { /* nothing to do if we encounter a superior version number than expected, just hope it is compatible */
			//TODO: Log this event, throw an execption?
			//TODO: use a table for versionning not the user_version pragma
		} else { /* Perform update if needed */
			// update the schema version in DB metadata, the above line shall work but if fails at runtime in soci lib on syntax error
			//sql<<"PRAGMA user_version = :versionNumber;", use(lime::settings::DBuserVersion);
			sql<<"PRAGMA user_version = "<<lime::settings::DBuserVersion<<";"; // This one does the job. This pragma is valid in sqlite3 but might not be with other soci backends

			// whole DB creation:
			transaction tr(sql); // begin a transaction which will hold all the create table queries (no sense in allowing the DB to be partially created)

			/*** Double Ratchet tables ***/
			/* DR Session:
			 *  - DId : Device Id is a foreign key from lime_PeerDevices table: peer device Id, allow to retrieve sessions linking to a peer device and a local account
			 *  - SessionId(primary key)
			 *  - Ns, Nr, PN : index for sending, receivind and previous sending chain
			 *  - DHr : peer current public ECDH key
60
			 *  - DHs : self current ECDH key. (public || private keys)
johan's avatar
johan committed
61
			 *  - RK, CKs, CKr : Root key, sender and receiver chain keys
62
			 *  - AD : Associated data : provided once at session creation by X3DH, is derived from initiator public Ik and id, receiver public Ik and id
johan's avatar
johan committed
63 64 65 66 67 68 69 70 71 72 73
			 *  - Status : 0 is for stale and 1 is for active, only one session shall be active for a peer device, by default created as active
			 *  - timeStamp : is updated when session change status and is used to remove stale session after determined time in cleaning operation
			 *  - X3DHInit : when we are initiator, store the generated X3DH init message and keep sending it until we've got at least a reply from peer
			 */
			sql<<"CREATE TABLE DR_sessions( \
						Did INTEGER NOT NULL DEFAULT 0, \
						sessionId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \
						Ns UNSIGNED INTEGER NOT NULL, \
						Nr UNSIGNED INTEGER NOT NULL, \
						PN UNSIGNED INTEGER NOT NULL, \
						DHr BLOB NOT NULL, \
74
						DHs BLOB NOT NULL, \
johan's avatar
johan committed
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
						RK BLOB NOT NULL, \
						CKs BLOB NOT NULL, \
						CKr BLOB NOT NULL, \
						AD BLOB NOT NULL, \
						Status INTEGER NOT NULL DEFAULT 1, \
						timeStamp DATETIME DEFAULT CURRENT_TIMESTAMP, \
						X3DHInit BLOB DEFAULT NULL, \
						FOREIGN KEY(Did) REFERENCES lime_PeerDevices(Did) ON UPDATE CASCADE ON DELETE CASCADE)";

			/* DR Message Skipped DH : DHid(primary key), SessionId, DHr */
			sql<<"CREATE TABLE DR_MSk_DHr( \
						DHid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \
						sessionId INTEGER NOT NULL DEFAULT 0, \
						DHr BLOB NOT NULL);";

			/* DR Message Skipped MK : [DHid,NR](primary key), MK */
			sql<<"CREATE TABLE DR_MSk_MK( \
						DHid INTEGER NOT NULL, \
						Nr INTEGER NOT NULL, \
						MK BLOB NOT NULL, \
						PRIMARY KEY( DHid , Nr ));";

			/*** Lime tables : local user identities, peer devices identities ***/
			/* List each self account enable on device :
			   - Uid : primary key, used to make link with Peer Devices, SPk and OPk tables
			   - User Id : shall be the GRUU
101
			   - Ik : public||private indentity key (EdDSA key)
johan's avatar
johan committed
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
			   - server : the URL of key Server
			   - curveId : identifies the curve used by this user - MUST be in sync with server
			*/
			sql<<"CREATE TABLE lime_LocalUsers( \
						Uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \
						UserId TEXT NOT NULL, \
						Ik BLOB NOT NULL, \
						server TEXT NOT NULL, \
						curveId INTEGER NOT NULL DEFAULT 0);"; // default the curveId value to 0 which is not one of the possible values(defined in lime.hpp)

			/* Peer Devices :
			 * - Did : primary key, used to make link with DR_sessions table.
			 * - DeviceId: peer device id (shall be its GRUU)
			 * - Uid: link to LocalUsers table, identify which localUser registered this peer Device
			 * - Ik : Peer device Identity public key, got it from X3DH server or X3DH init message
			 */
			sql<<"CREATE TABLE lime_PeerDevices( \
						Did INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, \
						DeviceId TEXT NOT NULL, \
						Uid INTEGER NOT NULL, \
						Ik BLOB NOT NULL, \
						FOREIGN KEY(Uid) REFERENCES lime_LocalUsers(Uid) ON UPDATE CASCADE ON DELETE CASCADE);";

			/*** X3DH tables ***/
			/* Signed pre-key :
			 * - SPKid : the primary key must be a random number as it is public, so avoid leaking information on number of key used
			 * - SPK : Public key||Private Key (ECDH keys)
			 * - timeStamp : Application shall renew SPK regurlarly(SPK_LifeTime). Old key are disactivated and deleted after a period(SPK_LimboTime))
			 * - Status : a boolean: can be active(1) or stale(0), by default any newly inserted key is set to active
			 * - Uid : User Id from lime_LocalUsers table: who's key is this
			 */
			sql<<"CREATE TABLE X3DH_SPK( \
						SPKid UNSIGNED INTEGER PRIMARY KEY NOT NULL, \
						SPK BLOB NOT NULL, \
						timeStamp DATETIME DEFAULT CURRENT_TIMESTAMP, \
						Status INTEGER NOT NULL DEFAULT 1, \
						Uid INTEGER NOT NULL, \
						FOREIGN KEY(Uid) REFERENCES lime_LocalUsers(Uid) ON UPDATE CASCADE ON DELETE CASCADE);";

			/* One time pre-key : deleted after usage, generated at user creation and on X3DH server request
			 * - OPKid : the primary key must be a random number as it is public, so avoid leaking information on number of key used
			 * - OPK : Public key||Private Key (ECDH keys)
			 * - Uid : User Id from lime_LocalUsers table: who's key is this
			 */
			sql<<"CREATE TABLE X3DH_OPK( \
						OPKid UNSIGNED INTEGER PRIMARY KEY NOT NULL, \
						OPK BLOB NOT NULL, \
						Uid INTEGER NOT NULL, \
						FOREIGN KEY(Uid) REFERENCES lime_LocalUsers(Uid) ON UPDATE CASCADE ON DELETE CASCADE);";

			tr.commit(); // commit all the previous queries
		}
	}
};
/******************************************************************************/
/*                                                                            */
/* Db public API                                                              */
/*                                                                            */
/******************************************************************************/
/**
 * @brief Check for existence, retrieve Uid for local user based on its userId(GRUU) and curve from table lime_LocalUsers
 *
 * @param[in]	userId		a string holding the user to look for in DB, shall be its GRUU
 * @param[out]	Uid		the DB internal Id matching given userId(if find in DB, 0 otherwise)
 * @param[out]	curveId		the curve selected at user creation
 *
 */
void Db::load_LimeUser(const std::string &userId, long int &Uid, lime::CurveId &curveId, std::string &url)
{
	int curve=0;
	sql<<"SELECT Uid,CurveId,server FROM lime_LocalUsers WHERE UserId = :userId LIMIT 1;", into(Uid), into(curve), into(url), use(userId);

	if (sql.got_data()) { // we found someone
		// turn back integer value retrieved from DB into a lime::CurveId
		switch (curve) {
			case static_cast<uint8_t>(lime::CurveId::c25519):
				curveId=lime::CurveId::c25519;
				break;
			case static_cast<uint8_t>(lime::CurveId::c448):
				curveId=lime::CurveId::c448;
				break;
			case static_cast<uint8_t>(lime::CurveId::unset):
			default: // we got an unknow or unset curve Id, DB is either corrupted or a future version
				curveId=lime::CurveId::unset;
				Uid=0;
				throw BCTBX_EXCEPTION << "Lime DB either corrupted or back from the future. User "<<userId<<" claim to run with unknown or unset Curve Id "<< curve;
		}
	} else { // no match: throw an execption
		Uid = 0; // be sure to reset the db_Uid to 0
		throw BCTBX_EXCEPTION << "Cannot find Lime User "<<userId<<" in DB";
	}
}

/**
 * @brief if exists, delete user
 *
 * @param[in]	userId		a string holding the user to look for in DB, shall be its GRUU
 *
 */
void Db::delete_LimeUser(const std::string &userId)
{
	sql<<"DELETE FROM lime_LocalUsers WHERE UserId = :userId;", use(userId);
}

/******************************************************************************/
/*                                                                            */
/* Double ratchet member functions                                            */
/*                                                                            */
/******************************************************************************/
template <typename DHKey>
bool DR<DHKey>::session_save() {

	// open transaction
	transaction tr(m_localStorage->sql);

	// shall we try to insert or update?
	bool MSk_DHr_Clean = false; // flag use to signal the need for late cleaning in DR_MSk_DHr table
	if (m_dbSessionId==0) { // We have no id for this session row, we shall insert a new one
		// Build blobs from DR session
		blob DHr(m_localStorage->sql);
		DHr.write(0, (char *)(m_DHr.data()), m_DHr.size());
223 224 225
		blob DHs(m_localStorage->sql);
		DHs.write(0, (char *)(m_DHs.publicKey().data()), m_DHs.publicKey().size()); // DHs holds Public || Private keys in the same field
		DHs.write(m_DHs.publicKey().size(), (char *)(m_DHs.privateKey().data()), m_DHs.privateKey().size());
johan's avatar
johan committed
226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241
		blob RK(m_localStorage->sql);
		RK.write(0, (char *)(m_RK.data()), m_RK.size());
		blob CKs(m_localStorage->sql);
		CKs.write(0, (char *)(m_CKs.data()), m_CKs.size());
		blob CKr(m_localStorage->sql);
		CKr.write(0, (char *)(m_CKr.data()), m_CKr.size());
		/* this one is written in base only at creation and never updated again */
		blob AD(m_localStorage->sql);
		AD.write(0, (char *)(m_sharedAD.data()), m_sharedAD.size());

		// make sure we have no other session active with this peer DiD
		m_localStorage->sql<<"UPDATE DR_sessions SET Status = 0, timeStamp = CURRENT_TIMESTAMP WHERE Did = :Did", use(m_peerDid);

		if (m_X3DH_initMessage.size()>0) {
			blob X3DH_initMessage(m_localStorage->sql);
			X3DH_initMessage.write(0, (char *)(m_X3DH_initMessage.data()), m_X3DH_initMessage.size());
242
			m_localStorage->sql<<"INSERT INTO DR_sessions(Ns,Nr,PN,DHr,DHs,RK,CKs,CKr,AD,Did,X3DHInit) VALUES(:Ns,:Nr,:PN,:DHr,:DHs,:RK,:CKs,:CKr,:AD,:Did,:X3DHinit);", use(m_Ns), use(m_Nr), use(m_PN), use(DHr), use(DHs), use(RK), use(CKs), use(CKr), use(AD), use(m_peerDid), use(X3DH_initMessage);
johan's avatar
johan committed
243
		} else {
244
			m_localStorage->sql<<"INSERT INTO DR_sessions(Ns,Nr,PN,DHr,DHs,RK,CKs,CKr,AD,Did) VALUES(:Ns,:Nr,:PN,:DHr,:DHs,:RK,:CKs,:CKr,:AD,:Did);", use(m_Ns), use(m_Nr), use(m_PN), use(DHr), use(DHs), use(RK), use(CKs), use(CKr), use(AD), use(m_peerDid);
johan's avatar
johan committed
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
		}
		// if insert went well we shall be able to retrieve the last insert id to save it in the Session object
		/*** WARNING: unportable section of code, works only with sqlite3 backend ***/
		m_localStorage->sql<<"select last_insert_rowid()",into(m_dbSessionId);
		/*** above could should work but it doesn't, consistently return false from .get_last_insert_id... ***/
		/*if (!(sql.get_last_insert_id("DR_sessions", m_dbSessionId))) {
			throw;
		} */
	} else { // we have an id, it shall already be in the db
		// Try to update an existing row
		try{ //TODO: make sure the update was a success, or we shall signal it
			switch (m_dirty) {
				case DRSessionDbStatus::dirty: // dirty case shall actually never occurs as a dirty is set only at creation not loading, first save is processed above
				case DRSessionDbStatus::dirty_ratchet: // ratchet&decrypt modifies all but also request to delete X3DHInit from storage
				{
					// make sure we have no other session active with this peer DiD
					if (m_active_status == false) {
						m_localStorage->sql<<"UPDATE DR_sessions SET Status = 0, timeStamp = CURRENT_TIMESTAMP WHERE Did = :Did", use(m_peerDid);
						m_active_status = true;
					}

					// Build blobs from DR session
					blob DHr(m_localStorage->sql);
					DHr.write(0, (char *)(m_DHr.data()), m_DHr.size());
269 270 271
					blob DHs(m_localStorage->sql);
					DHs.write(0, (char *)(m_DHs.publicKey().data()), m_DHs.publicKey().size()); // DHs holds Public || Private keys in the same field
					DHs.write(m_DHs.publicKey().size(), (char *)(m_DHs.privateKey().data()), m_DHs.privateKey().size());
johan's avatar
johan committed
272 273 274 275 276 277 278
					blob RK(m_localStorage->sql);
					RK.write(0, (char *)(m_RK.data()), m_RK.size());
					blob CKs(m_localStorage->sql);
					CKs.write(0, (char *)(m_CKs.data()), m_CKs.size());
					blob CKr(m_localStorage->sql);
					CKr.write(0, (char *)(m_CKr.data()), m_CKr.size());

279
					m_localStorage->sql<<"UPDATE DR_sessions SET Ns= :Ns, Nr= :Nr, PN= :PN, DHr= :DHr,DHs= :DHs, RK= :RK, CKs= :CKs, CKr= :CKr, Status = 1,  X3DHInit = NULL WHERE sessionId = :sessionId;", use(m_Ns), use(m_Nr), use(m_PN), use(DHr), use(DHs), use(RK), use(CKs), use(CKr), use(m_dbSessionId);
johan's avatar
johan committed
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
				}
					break;
				case DRSessionDbStatus::dirty_decrypt: // decrypt modifies: CKr and Nr. Also set Status to active and clear X3DH init message if there is one(it is actually useless as our first reply from peer shall trigger a ratchet&decrypt)
				{
					// make sure we have no other session active with this peer DiD
					if (m_active_status == false) {
						m_localStorage->sql<<"UPDATE DR_sessions SET Status = 0, timeStamp = CURRENT_TIMESTAMP WHERE Did = :Did", use(m_peerDid);
						m_active_status = true;
					}

					blob CKr(m_localStorage->sql);
					CKr.write(0, (char *)(m_CKr.data()), m_CKr.size());
					m_localStorage->sql<<"UPDATE DR_sessions SET Nr= :Nr, CKr= :CKr, Status = 1, X3DHInit = NULL WHERE sessionId = :sessionId;", use(m_Nr), use(CKr), use(m_dbSessionId);
				}
					break;
				case DRSessionDbStatus::dirty_encrypt: // encrypt modifies: CKs and Ns
				{
					blob CKs(m_localStorage->sql);
					CKs.write(0, (char *)(m_CKs.data()), m_CKs.size());
299
					m_localStorage->sql<<"UPDATE DR_sessions SET Ns= :Ns, CKs= :CKs, Status = :active_status WHERE sessionId = :sessionId;", use(m_Ns), use(CKs), use((m_active_status==true)?0x01:0x00), use(m_dbSessionId);
johan's avatar
johan committed
300 301
				}
					break;
302 303
				case DRSessionDbStatus::clean: // Session is clean? So why have we been called?
				default:
johan's avatar
johan committed
304
					BCTBX_SLOGE<<"Double ratchet session saved call on sessionId "<<m_dbSessionId<<" but sessions appears to be clean";
johan's avatar
johan committed
305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
					break;
			}
		} catch (...) {
			throw;
		}
		// updatesert went well, do we have any mkskipped row to modify
		if (m_usedDHid !=0 ) { // ok, we consumed a key, remove it from db
			m_localStorage->sql<<"DELETE from DR_MSk_MK WHERE DHid = :DHid AND Nr = :Nr;", use(m_usedDHid), use(m_usedNr);
			MSk_DHr_Clean = true; // flag the cleaning needed in DR_MSk_DH table, we may have to remove a row in it if no more row are linked to it in DR_MSk_MK
		}
	}

	// Shall we insert some skipped Message keys?
	for ( auto rChain : m_mkskipped) { // loop all chains of message keys, each one is a DHr associated to an unordered map of MK indexed by Nr to be saved
		blob DHr(m_localStorage->sql);
		DHr.write(0, (char *)(rChain.DHr.data()), rChain.DHr.size());
		long DHid=0;
		m_localStorage->sql<<"SELECT DHid FROM DR_MSk_DHr WHERE sessionId = :sessionId AND DHr = :DHr LIMIT 1",into(DHid), use(m_dbSessionId), use(DHr);
		if (!m_localStorage->sql.got_data()) { // There is no row in DR_MSk_DHr matching this key, we must add it
			m_localStorage->sql<<"INSERT INTO DR_MSk_DHr(sessionId, DHr) VALUES(:sessionId, :DHr)", use(m_dbSessionId), use(DHr);
			m_localStorage->sql<<"select last_insert_rowid()",into(DHid); // WARNING: unportable code, sqlite3 only, see above for more details on similar issue
		}
		// insert all the skipped key in the chain
328
		uint16_t Nr;
johan's avatar
johan committed
329 330 331 332 333 334 335 336 337 338 339 340
		blob MK(m_localStorage->sql);
		statement st = (m_localStorage->sql.prepare << "INSERT INTO DR_MSk_MK(DHid,Nr,MK) VALUES(:DHid,:Nr,:Mk)", use(DHid), use(Nr), use(MK));

		for (const auto &kv : rChain.messageKeys) { // messageKeys is an unordered map of MK indexed by Nr.
			Nr=kv.first;
			MK.write(0, (char *)kv.second.data(), kv.second.size());
			st.execute(true);
		}
	}

	// Now do the cleaning(remove unused row from DR_MKs_DHr table) if needed
	if (MSk_DHr_Clean == true) {
341
		uint16_t Nr;
johan's avatar
johan committed
342 343 344 345 346 347 348 349 350 351 352 353 354 355
		m_localStorage->sql<<"SELECT Nr from DR_MSk_MK WHERE DHid = :DHid LIMIT 1;", into(Nr), use(m_usedDHid);
		if (!m_localStorage->sql.got_data()) { // no more MK with this DHid, remove it
			m_localStorage->sql<<"DELETE from DR_MSk_DHr WHERE DHid = :DHid;", use(m_usedDHid);
		}
	}

	tr.commit();
	return true;
};

template <typename DHKey>
bool DR<DHKey>::session_load() {
	// blobs to store DR session data
	blob DHr(m_localStorage->sql);
356
	blob DHs(m_localStorage->sql);
johan's avatar
johan committed
357 358 359 360 361 362 363 364 365
	blob RK(m_localStorage->sql);
	blob CKs(m_localStorage->sql);
	blob CKr(m_localStorage->sql);
	blob AD(m_localStorage->sql);
	blob X3DH_initMessage(m_localStorage->sql);

	// create an empty DR session
	indicator ind;
	int status; // retrieve an int from DB, turn it into a bool to store in object
366
	m_localStorage->sql<<"SELECT Did,Ns,Nr,PN,DHr,DHs,RK,CKs,CKr,AD,Status,X3DHInit FROM DR_sessions WHERE sessionId = :sessionId LIMIT 1", into(m_peerDid), into(m_Ns), into(m_Nr), into(m_PN), into(DHr), into(DHs), into(RK), into(CKs), into(CKr), into(AD), into(status), into(X3DH_initMessage,ind), use(m_dbSessionId);
johan's avatar
johan committed
367 368 369

	if (m_localStorage->sql.got_data()) { // TODO : some more specific checks on length of retrieved data?
		DHr.read(0, (char *)(m_DHr.data()), m_DHr.size());
370 371
		DHs.read(0, (char *)(m_DHs.publicKey().data()), m_DHs.publicKey().size());
		DHs.read(m_DHs.publicKey().size(), (char *)(m_DHs.privateKey().data()), m_DHs.privateKey().size());
johan's avatar
johan committed
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
		RK.read(0, (char *)(m_RK.data()), m_RK.size());
		CKs.read(0, (char *)(m_CKs.data()), m_CKs.size());
		CKr.read(0, (char *)(m_CKr.data()), m_CKr.size());
		AD.read(0, (char *)(m_sharedAD.data()), m_sharedAD.size());
		if (ind == i_ok && X3DH_initMessage.get_len()>0) {
			m_X3DH_initMessage.resize(X3DH_initMessage.get_len());
			X3DH_initMessage.read(0, (char *)(m_X3DH_initMessage.data()), m_X3DH_initMessage.size());
		}
		if (status==1) {
			m_active_status = true;
		} else {
			m_active_status = false;
		}
		return true;
	} else { // something went wrong with the DB, we cannot retrieve the session
		return false;
	}
};

template <typename Curve>
392
bool DR<Curve>::trySkippedMessageKeys(const uint16_t Nr, const X<Curve> &DHr, DRMKey &MK) {
johan's avatar
johan committed
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
	blob MK_blob(m_localStorage->sql);
	blob DHr_blob(m_localStorage->sql);
	DHr_blob.write(0, (char *)(DHr.data()), DHr.size());

	indicator ind;
	m_localStorage->sql<<"SELECT m.MK, m.DHid FROM DR_MSk_MK as m INNER JOIN DR_MSk_DHr as d ON d.DHid=m.DHid WHERE d.sessionId = :sessionId AND d.DHr = :DHr AND m.Nr = :Nr LIMIT 1", into(MK_blob,ind), into(m_usedDHid), use(m_dbSessionId), use(DHr_blob), use(Nr);
	// we didn't find anything
	if (!m_localStorage->sql.got_data() || ind != i_ok || MK_blob.get_len()!=MK.size()) {
		m_usedDHid=0; // make sure the DHid is not set when we didn't find anything as it is later used to remove confirmed used key from DB
		return false;
	}
	// record the Nr of extracted to be able to delete it fron base later(if decrypt ends well)
	m_usedNr=Nr;

	MK_blob.read(0, (char *)(MK.data()), MK.size());
	return true;
};
/* template instanciations for Curves 25519 and 448 */
#ifdef EC25519_ENABLED
	template bool DR<C255>::session_load();
	template bool DR<C255>::session_save();
414
	template bool DR<C255>::trySkippedMessageKeys(const uint16_t Nr, const X<C255> &DHr, DRMKey &MK);
johan's avatar
johan committed
415 416 417 418 419
#endif

#ifdef EC448_ENABLED
	template bool DR<C448>::session_load();
	template bool DR<C448>::session_save();
420
	template bool DR<C448>::trySkippedMessageKeys(const uint16_t Nr, const X<C448> &DHr, DRMKey &MK);
johan's avatar
johan committed
421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
#endif

/******************************************************************************/
/*                                                                            */
/*  Lime members functions                                                    */
/*                                                                            */
/******************************************************************************/
/**
 * @brief Create a new local user based on its userId(GRUU) from table lime_LocalUsers
 *
 * use m_selfDeviceId as input, use m_X3DH_Server as input
 * populate m_db_Uid
 *
 * @return true if user was created successfully, exception is thrown otherwise.
 *
 * @exception BCTBX_EXCEPTION	thrown if user already exists in the database
 */
template <typename Curve>
bool Lime<Curve>::create_user()
{
	// check if the user is not already in the DB
	int dummy_Uid;
	m_localStorage->sql<<"SELECT Uid FROM lime_LocalUsers WHERE UserId = :userId LIMIT 1;", into(dummy_Uid), use(m_selfDeviceId);
	if (m_localStorage->sql.got_data()) {
		throw BCTBX_EXCEPTION << "Lime user "<<m_selfDeviceId<<" cannot be created: it is already in Database - delete it before if you really want to replace it";
	}

	// generate an identity EDDSA key pair
	auto EDDSAContext = EDDSAInit<Curve>();
	bctbx_EDDSACreateKeyPair(EDDSAContext, (int (*)(void *, uint8_t *, size_t))bctbx_rng_get, m_RNG);
	// store it in a blob : Public||Private
	blob Ik(m_localStorage->sql);
	Ik.write(0, (const char *)EDDSAContext->publicKey, EDDSAContext->pointCoordinateLength);
	Ik.write(EDDSAContext->pointCoordinateLength, (const char *)EDDSAContext->secretKey, EDDSAContext->secretLength);
	/* set the Ik in Lime object */
	//m_Ik = std::move(KeyPair<ED<Curve>>{EDDSAContext->publicKey, EDDSAContext->secretKey});
	bctbx_DestroyEDDSAContext(EDDSAContext);


	// insert in DB
	try {
		m_localStorage->sql<<"INSERT INTO lime_LocalUsers(UserId,Ik,server,curveId) VALUES (:userId,:Ik,:server,:curveId) ", use(m_selfDeviceId), use(Ik), use(m_X3DH_Server_URL), use(static_cast<uint8_t>(Curve::curveId()));
	} catch (exception const &e) {
		throw BCTBX_EXCEPTION << "Lime user insertion failed. DB backend says : "<<e.what();
	}
	// get the Id of inserted row
	m_localStorage->sql<<"select last_insert_rowid()",into(m_db_Uid);
	/* WARNING: previous line break portability of DB backend, specific to sqlite3.
	Following code shall work but consistently returns false and do not set m_db_Uid...*/
	/*
	if (!(m_localStorage->sql.get_last_insert_id("lime_LocalUsers", m_db_Uid)))
		throw BCTBX_EXCEPTION << "Lime user insertion failed. Couldn't retrieve last insert DB";
	}
	*/
	/* all went fine set the Ik loaded flag */
	//m_Ik_loaded = true;


	return true;
}

template <typename Curve>
void Lime<Curve>::get_SelfIdentityKey() {
	if (m_Ik_loaded == false) {
		blob Ik_blob(m_localStorage->sql);
		m_localStorage->sql<<"SELECT Ik FROM Lime_LocalUsers WHERE Uid = :UserId LIMIT 1;", into(Ik_blob), use(m_db_Uid);
		if (m_localStorage->sql.got_data()) { // Found it, it is stored in one buffer Public || Private
			Ik_blob.read(0, (char *)(m_Ik.publicKey().data()), m_Ik.publicKey().size()); // Read the public key
			Ik_blob.read(m_Ik.publicKey().size(), (char *)(m_Ik.privateKey().data()), m_Ik.privateKey().size()); // Read the private key
			m_Ik_loaded = true; // set the flag
		}
	}
}


template <typename Curve>
void Lime<Curve>::X3DH_generate_SPk(X<Curve> &publicSPk, Signature<Curve> &SPk_sig, uint32_t &SPk_id) {
	// check Identity key is loaded in Lime object context
	get_SelfIdentityKey();

	// Generate a new ECDH Key pair
	auto ECDH_Context = ECDHInit<Curve>();
	bctbx_ECDHCreateKeyPair(ECDH_Context, (int (*)(void *, uint8_t *, size_t))bctbx_rng_get, m_RNG);

	// Sign the public key with our identity key
	auto EDDSA_Context = EDDSAInit<Curve>();
	bctbx_EDDSA_setPublicKey(EDDSA_Context, m_Ik.publicKey().data(), m_Ik.publicKey().size());
	bctbx_EDDSA_setSecretKey(EDDSA_Context, m_Ik.privateKey().data(), m_Ik.privateKey().size());
	auto sig_size=SPk_sig.size();
	bctbx_EDDSA_sign(EDDSA_Context, ECDH_Context->selfPublic, ECDH_Context->pointCoordinateLength, nullptr, 0, SPk_sig.data(), &sig_size);

	// Generate a random SPk Id
	std::array<uint8_t,4> randomId;
	bctbx_rng_get(m_RNG, randomId.data(), randomId.size());
	SPk_id = static_cast<uint32_t>(randomId[0])<<24 | static_cast<uint32_t>(randomId[1])<<16 | static_cast<uint32_t>(randomId[2])<<8 | static_cast<uint32_t>(randomId[3]);

	// insert all this in DB
	try {
		// open a transaction as both modification shall be done or none
		transaction tr(m_localStorage->sql);

		// We must first update potential existing SPK in base from active to stale status
		m_localStorage->sql<<"UPDATE X3DH_SPK SET Status = 0, timeStamp = CURRENT_TIMESTAMP WHERE Uid = :Uid", use(m_db_Uid);

		blob SPk_blob(m_localStorage->sql);
		SPk_blob.write(0, (const char *)ECDH_Context->selfPublic, ECDH_Context->pointCoordinateLength);
		SPk_blob.write(ECDH_Context->pointCoordinateLength, (const char *)ECDH_Context->secret, ECDH_Context->secretLength);
		m_localStorage->sql<<"INSERT INTO X3DH_SPK(SPKid,SPK,Uid) VALUES (:SPKid,:SPK,:Uid) ", use(SPk_id), use(SPk_blob), use(m_db_Uid);

		tr.commit();
	} catch (exception const &e) {
		bctbx_DestroyECDHContext(ECDH_Context);
		bctbx_DestroyEDDSAContext(EDDSA_Context);
		throw BCTBX_EXCEPTION << "SPK insertion in DB failed. DB backend says : "<<e.what();
	}

	// get SPk public key in output param
	publicSPk = std::move(X<Curve>{ECDH_Context->selfPublic});

	// destroy contexts
	bctbx_DestroyECDHContext(ECDH_Context);
	bctbx_DestroyEDDSAContext(EDDSA_Context);
}

template <typename Curve>
void Lime<Curve>::X3DH_generate_OPks(std::vector<X<Curve>> &publicOPks, std::vector<uint32_t> &OPk_ids, const uint16_t OPk_number) {

	// make room for OPk and OPk ids
	publicOPks.reserve(OPk_number);
	OPk_ids.reserve(OPk_number);

	// Prepare DB statement
	transaction tr(m_localStorage->sql);
	blob OPk(m_localStorage->sql);
	uint32_t OPk_id;
	statement st = (m_localStorage->sql.prepare << "INSERT INTO X3DH_OPK(OPKid, OPK,Uid) VALUES(:OPKid,:OPK,:Uid)", use(OPk_id), use(OPk), use(m_db_Uid));

	// Create an ECDH context to create key pairs
	auto ECDH_Context = ECDHInit<Curve>();

	try {
		for (uint16_t i=0; i<OPk_number; i++) {
			// Generate a new ECDH Key pair
			bctbx_ECDHCreateKeyPair(ECDH_Context, (int (*)(void *, uint8_t *, size_t))bctbx_rng_get, m_RNG);

			// Generate a random SPk Id (uint32_t)
			std::array<uint8_t,4> randomId;
			bctbx_rng_get(m_RNG, randomId.data(), randomId.size());
			OPk_id = static_cast<uint32_t>(randomId[0])<<24 | static_cast<uint32_t>(randomId[1])<<16 | static_cast<uint32_t>(randomId[2])<<8 | static_cast<uint32_t>(randomId[3]);

			// Insert in DB: store Public Key || Private Key
			OPk.write(0, (char *)(ECDH_Context->selfPublic), ECDH_Context->pointCoordinateLength);
			OPk.write(ECDH_Context->pointCoordinateLength, (char *)(ECDH_Context->secret), ECDH_Context->secretLength);
			st.execute(true);

			// set in output vectors
			publicOPks.emplace_back(ECDH_Context->selfPublic);
			OPk_ids.push_back(OPk_id);
		}
	} catch (exception &e) {
		bctbx_DestroyECDHContext(ECDH_Context);
		throw BCTBX_EXCEPTION << "OPK insertion in DB failed. DB backend says : "<<e.what();
	}
	// commit changes to DB
	tr.commit();

	bctbx_DestroyECDHContext(ECDH_Context);
}

template <typename Curve>
void Lime<Curve>::cache_DR_sessions(std::vector<recipientInfos<Curve>> &internal_recipients, std::vector<std::string> &missing_devices) {
	/* build a user list of missing ones : produce a list ready to be sent to SQL query: 'user','user','user',... also build a map to store shared_ptr to sessions */
	std::string sqlString_requestedDevices{""};
	std::unordered_map<std::string, std::shared_ptr<DR<Curve>>> requestedDevices; // found session will be loaded and temp stored in this

	size_t requestedDevicesCount = 0;
	for (auto &recipient : internal_recipients) {
		if (recipient.DRSession == nullptr) {
			sqlString_requestedDevices.append("'").append(recipient.deviceId).append("',");
			requestedDevicesCount++;
		}
	}
	if (requestedDevicesCount==0) return; // we already got them all

	sqlString_requestedDevices.pop_back(); // remove the last ','

	/* fetch them from DB */
608 609 610 611 612 613 614 615
	rowset<row> rs = (m_localStorage->sql.prepare << "SELECT s.sessionId, d.DeviceId FROM DR_sessions as s INNER JOIN lime_PeerDevices as d ON s.Did=d.Did WHERE d.Uid= :Uid AND s.Status=1 AND d.DeviceId IN ("<<sqlString_requestedDevices<<");", use(m_db_Uid));
	for (auto &r : rs) {
		auto sessionId = r.get<int>(0);
		auto peerDeviceId = r.get<string>(1);

		auto DRsession = std::make_shared<DR<Curve>>(m_localStorage.get(), sessionId); // load session from local storage
		requestedDevices[peerDeviceId] = DRsession; // store found session in a our temp container
		m_DR_sessions_cache[peerDeviceId] = DRsession; // session is also stored in cache
johan's avatar
johan committed
616 617 618 619 620 621 622 623 624
	}

	/* loop on internal recipient and fill it with the found ones, store the missing ones in the missing_devices vector */
	for (auto &recipient : internal_recipients) {
		if (recipient.DRSession == nullptr) { // they are missing
			auto retrievedElem = requestedDevices.find(recipient.deviceId);
			if (retrievedElem == requestedDevices.end()) { // we didn't found this one
				missing_devices.push_back(recipient.deviceId);
			} else { // we got this one
625
				recipient.DRSession = std::move(retrievedElem->second); // don't need this pointer in tmp comtainer anymore
johan's avatar
johan committed
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
			}
		}
	}
}

/**
 * @brief Store peer device information(DeviceId - GRUU -, public Ik, Uid to link it to a user) in local storage
 *
 * @param[in] peerDeviceId	The device id to insert
 * @param[in] Ik		The public EDDSA identity key of this device
 *
 * @return the id internally used by db to store this row
 */
template <typename Curve>
long int Lime<Curve>::store_peerDevice(const std::string &peerDeviceId, const ED<Curve> &Ik) {
	blob Ik_blob(m_localStorage->sql);

	try {
		long int Did=0;
		// make sure this device wasn't already here, if it was, check they have the same Ik
		m_localStorage->sql<<"SELECT Ik,Did FROM lime_peerDevices WHERE DeviceId = :DeviceId AND Uid = :Uid LIMIT 1;", into(Ik_blob), into(Did), use(peerDeviceId), use(m_db_Uid);
		if (m_localStorage->sql.got_data()) { // Found one
			ED<Curve> stored_Ik;
			Ik_blob.read(0, (char *)(stored_Ik.data()), stored_Ik.size()); // Read it to compare it to the given one
			if (stored_Ik == Ik) { // they match, so we just return the Did
				return Did;
			} else { // Ik are not matching, peer device changed its Ik!?! Reject
johan's avatar
johan committed
653
				BCTBX_SLOGE<<"It appears that peer device "<<peerDeviceId<<" was known with an identity key but is trying to use another one now";
johan's avatar
johan committed
654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672
				throw BCTBX_EXCEPTION << "Peer device "<<peerDeviceId<<" changed its Ik";
			}
		} else { // not found in local Storage
			transaction tr(m_localStorage->sql);
			Ik_blob.write(0, (char *)(Ik.data()), Ik.size());
			m_localStorage->sql<<"INSERT INTO lime_PeerDevices(DeviceId,Uid,Ik) VALUES (:deviceId,:Uid,:Ik) ", use(peerDeviceId), use(m_db_Uid), use(Ik_blob);
			m_localStorage->sql<<"select last_insert_rowid()",into(Did);
			tr.commit();
			bctbx_debug("user %s store peerDevice %s with device id %x", m_selfDeviceId.data(), peerDeviceId.data(), Did);
			return Did;
		}
	} catch (exception const &e) {
		throw BCTBX_EXCEPTION << "Peer device "<<peerDeviceId<<" insertion failed. DB backend says : "<<e.what();
	}
}

// load from local storage in DRSessions all DR session matching the peerDeviceId, ignore the one picked by id in 2nd arg
template <typename Curve>
void Lime<Curve>::get_DRSessions(const std::string &senderDeviceId, const long int ignoreThisDRSessionId, std::vector<std::shared_ptr<DR<Curve>>> &DRSessions) {
673 674 675 676 677
	rowset<int> rs = (m_localStorage->sql.prepare << "SELECT s.sessionId FROM DR_sessions as s INNER JOIN lime_PeerDevices as d ON s.Did=d.Did WHERE d.DeviceId = :senderDeviceId AND s.sessionId <> :ignoreThisDRSessionId ORDER BY s.Status DESC, timeStamp ASC;", use(senderDeviceId), use(ignoreThisDRSessionId));

	for (auto sessionId : rs) {
		/* load session in cache DRSessions */
		DRSessions.push_back(make_shared<DR<Curve>>(m_localStorage.get(), sessionId)); // load session from cache
johan's avatar
johan committed
678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746
	}
};

/**
 * @brief retrieve matching SPk from localStorage, throw an exception if not found
 *
 * @param[in]	SPk_id	Id of the SPk we're trying to fetch
 * @param[out]	SPk	The SPk if found
 */
template <typename Curve>
void Lime<Curve>::X3DH_get_SPk(uint32_t SPk_id, KeyPair<X<Curve>> &SPk) {
	blob SPk_blob(m_localStorage->sql);
	m_localStorage->sql<<"SELECT SPk FROM X3DH_SPk WHERE Uid = :Uid AND SPKid = :SPk_id LIMIT 1;", into(SPk_blob), use(m_db_Uid), use(SPk_id);
	if (m_localStorage->sql.got_data()) { // Found it, it is stored in one buffer Public || Private
		SPk_blob.read(0, (char *)(SPk.publicKey().data()), SPk.publicKey().size()); // Read the public key
		SPk_blob.read(SPk.publicKey().size(), (char *)(SPk.privateKey().data()), SPk.privateKey().size()); // Read the private key
	} else {
		throw BCTBX_EXCEPTION << "X3DH "<<m_selfDeviceId<<"look up for SPk id "<<SPk_id<<" failed";
	}
}


/**
 * @brief retrieve matching OPk from localStorage, throw an exception if not found
 * 	Note: once fetch, the OPk is deleted from localStorage
 *
 * @param[in]	OPk_id	Id of the OPk we're trying to fetch
 * @param[out]	OPk	The OPk if found
 */
template <typename Curve>
void Lime<Curve>::X3DH_get_OPk(uint32_t OPk_id, KeyPair<X<Curve>> &OPk) {
	blob OPk_blob(m_localStorage->sql);
	m_localStorage->sql<<"SELECT OPk FROM X3DH_OPk WHERE Uid = :Uid AND OPKid = :OPk_id LIMIT 1;", into(OPk_blob), use(m_db_Uid), use(OPk_id);
	if (m_localStorage->sql.got_data()) { // Found it, it is stored in one buffer Public || Private
		OPk_blob.read(0, (char *)(OPk.publicKey().data()), OPk.publicKey().size()); // Read the public key
		OPk_blob.read(OPk.publicKey().size(), (char *)(OPk.privateKey().data()), OPk.privateKey().size()); // Read the private key
		m_localStorage->sql<<"DELETE FROM X3DH_OPk WHERE Uid = :Uid AND OPKid = :OPk_id;", use(m_db_Uid), use(OPk_id); // And remove it from local Storage
	} else {
		throw BCTBX_EXCEPTION << "X3DH "<<m_selfDeviceId<<"look up for OPk id "<<OPk_id<<" failed";
	}
}

/* template instanciations for Curves 25519 and 448 */
#ifdef EC25519_ENABLED
	template bool Lime<C255>::create_user();
	template void Lime<C255>::get_SelfIdentityKey();
	template void Lime<C255>::X3DH_generate_SPk(X<C255> &publicSPk, Signature<C255> &SPk_sig, uint32_t &SPk_id);
	template void Lime<C255>::X3DH_generate_OPks(std::vector<X<C255>> &publicOPks, std::vector<uint32_t> &OPk_ids, const uint16_t OPk_number);
	template void Lime<C255>::cache_DR_sessions(std::vector<recipientInfos<C255>> &internal_recipients, std::vector<std::string> &missing_devices);
	template long int Lime<C255>::store_peerDevice(const std::string &peerDeviceId, const ED<C255> &Ik);
	template void Lime<C255>::get_DRSessions(const std::string &senderDeviceId, const long int ignoreThisDBSessionId, std::vector<std::shared_ptr<DR<C255>>> &DRSessions);
	template void Lime<C255>::X3DH_get_SPk(uint32_t SPk_id, KeyPair<X<C255>> &SPk);
	template void Lime<C255>::X3DH_get_OPk(uint32_t OPk_id, KeyPair<X<C255>> &SPk);
#endif

#ifdef EC448_ENABLED
	template bool Lime<C448>::create_user();
	template void Lime<C448>::get_SelfIdentityKey();
	template void Lime<C448>::X3DH_generate_SPk(X<C448> &publicSPk, Signature<C448> &SPk_sig, uint32_t &SPk_id);
	template void Lime<C448>::X3DH_generate_OPks(std::vector<X<C448>> &publicOPks, std::vector<uint32_t> &OPk_ids, const uint16_t OPk_number);
	template void Lime<C448>::cache_DR_sessions(std::vector<recipientInfos<C448>> &internal_recipients, std::vector<std::string> &missing_devices);
	template long int Lime<C448>::store_peerDevice(const std::string &peerDeviceId, const ED<C448> &Ik);
	template void Lime<C448>::get_DRSessions(const std::string &senderDeviceId, const long int ignoreThisDBSessionId, std::vector<std::shared_ptr<DR<C448>>> &DRSessions);
	template void Lime<C448>::X3DH_get_SPk(uint32_t SPk_id, KeyPair<X<C448>> &SPk);
	template void Lime<C448>::X3DH_get_OPk(uint32_t OPk_id, KeyPair<X<C448>> &SPk);
#endif


} // namespace lime