introduce Distribution.Echo and Media key, ported from Release_2.1, not completely cleaned up yet

doxygen-3.x
positron 3 months ago
parent ba42c3f339
commit 87c0a3f17c
  1. 3
      .gitignore
  2. 4
      asn.1/Makefile
  3. 19
      codegen/distribution.fsm
  4. 1
      src/Makefile
  5. 49
      src/baseprotocol.c
  6. 3
      src/baseprotocol.h
  7. 27
      src/distribution_api.h
  8. 662
      src/echo_api.c
  9. 197
      src/echo_api.h
  10. 4
      src/keymanagement.c
  11. 497
      src/media_key.c
  12. 309
      src/media_key.h
  13. 85
      src/media_key_example.h
  14. 285
      src/message_api.c
  15. 40
      src/pEpEngine.c
  16. 29
      src/pEpEngine.h
  17. 8
      src/pEp_internal.h
  18. 19
      src/platform.h
  19. 10
      src/platform_unix.c
  20. 14
      src/platform_windows.cpp
  21. 6
      src/sync_api.h
  22. 140
      test/src/EchoTest.cc
  23. 234
      test/src/MediaKeyTest.cc

3
.gitignore vendored

@ -150,3 +150,6 @@ sync/.copy
# Will remove once KER comes in
test/test_mails/default_keys_test_*.eml
# I prefix my off-git files and directories with an underscore.
_*

@ -21,7 +21,7 @@ ifeq ($(BUILD_FOR),Linux)
CPPFLAGS+=-U_REENTRANT
endif
DISTRIBUTION = distribution keyreset managedgroup exploration
DISTRIBUTION = distribution keyreset managedgroup exploration echo
SYNC = sync keysync trustsync groupsync
STORAGE = storage messagestorage
@ -40,7 +40,7 @@ libasn1.a: $(ALL_OBJECTS)
%.o: %.c %.h
$(CC) $(CFLAGS) $(CPPFLAGS) $(OPTIMIZE) $(ASN1C_INC) -c $< -o $@
Sync.c: $(SYNC_FILES) $(DISTRIBUTION_FILES) pEp.asn1
Sync.c: $(SYNC_FILES) $(DISTRIBUTION_FILES) pEp.asn1 # positrontofdik: is it correct that $(DISTRIBUTION_FILES) is among the dependencies? I would guess not
$(ASN1C) -gen-PER $(ASN1C_OPTIONS) $+
rm -f converter-sample.c
touch Sync.c

@ -68,5 +68,24 @@ protocol Distribution 2 {
field Identity back;
}
}
/* The Echo protocol does not delvier any security property; it is only
used to initiate TOFU. The Echo protocol partly destroys privacy --
that is why it can be switched off, session-wise. */
fsm Echo 5 {
version 1, 0;
message EchoPing 2 {
field TID challenge;
/* Naïf comment for positron, to delete:
We should rely on recv_by: we do not have a recipient identity
specified here, and we should not rely on such information
anyway. */
}
message EchoPong 3 {
field TID challenge;
}
}
}

@ -121,6 +121,7 @@ HEADERS_TO_INSTALL = \
cryptotech.h sync_api.h pEp_string.h openpgp_compat.h engine_sql.h \
labeled_int_list.h key_reset.h base64.h sync_codec.h distribution_codec.h \
message_codec.h storage_codec.h status_to_string.h keyreset_command.h \
echo_api.h distribution_api.h media_key.h \
map_asn1.h \
platform.h platform_unix.h platform_windows.h platform_zos.h \
transport.h growing_buf.h $(wildcard ../asn.1/*.h)

@ -12,6 +12,10 @@
#include "pEp_internal.h"
#include "message_api.h"
#include "baseprotocol.h"
#include "Distribution.h" // for Distribution_t
#include "distribution_codec.h" // for decode_Distribution_message
#include "status_to_string.h" // for decode_Distribution_message
/**
* @internal
@ -152,7 +156,10 @@ PEP_STATUS base_prepare_message(
if (!msg->to)
goto enomem;
msg->shortmsg = strdup("p≡p key management message - please ignore");
if (type == BASE_SYNC)
msg->shortmsg = strdup("p≡p key management message (Sync) - please ignore");
else
msg->shortmsg = strdup("p≡p key management message (Distribution) - please ignore");
assert(msg->shortmsg);
if (!msg->shortmsg)
goto enomem;
@ -231,10 +238,48 @@ PEP_STATUS base_extract_message(
if (!(_payload && _payload_size))
goto the_end;
/* We need to check the signature and drop a message with an invalid or
missing signature if the protocol is one of:
- Sync.Sync;
- Distribution.Key_reset
but *not* if the protocol is
- Distribytion.Echo.
Here we know the family (Sync vs Distribution) but not the actual
protcol. Unfortunately we need to decode the payload here just to
check, in case the family is Distribution. A little wasteful, but
not terribly important: this engine branch will not live long, and
v3 does not need this same hack. */
char *_fpr = NULL;
if (_sign) {
bool _require_signature;
switch (type) {
case BASE_SYNC:
_require_signature = true;
break;
case BASE_DISTRIBUTION: {
Distribution_t *_dist = NULL;
status = decode_Distribution_message(_payload, _payload_size, &_dist);
if (status != PEP_STATUS_OK) {
fprintf(stderr, "B base_extract_message: about the message %s: this should not happen: status is %i %s\n", msg->shortmsg, status, pEp_status_to_string(status));
goto the_end;
}
switch (_dist->present) {
case Distribution_PR_keyreset:
_require_signature = true; break;
case Distribution_PR_echo:
_require_signature = false; break;
default:
assert(false);
}
ASN_STRUCT_FREE(asn_DEF_Distribution, _dist);
break;
}
default:
assert(false);
}
if (_require_signature && _sign) {
status = verify_text(session, _payload, _payload_size, _sign, _sign_size, &keylist);
if (!(status == PEP_VERIFIED || status == PEP_VERIFIED_AND_TRUSTED) || !keylist || !keylist->value) {
fprintf(stderr,"Q %s: SIGNATURE MISMATCH: THIS IS NOT NORMAL.\n", (msg->shortmsg ? msg->shortmsg : "<NO SUBJECT>"));
// signature invalid or does not match; ignore message
status = PEP_STATUS_OK;
goto the_end;

@ -84,7 +84,8 @@ PEP_STATUS base_decorate_message(
* @param[in] type base protocol type
* @param[in] payload payload to send
* @param[in] size size of payload
* @param[in] fpr optional key to sign or NULL
* @param[in] fpr optional key to sign or NULL;
* the message will not be signed if NULL
* @param[out] result returned message with payload on success
*
* @retval PEP_STATUS_OK on success

@ -0,0 +1,27 @@
/**
* @file distribution_api.h
* @brief Distribution-protocol API
* @license GNU General Public License 3.0 - see LICENSE.txt
*
* A convenience header including headers for all the protocol in the
* distribution family.
*/
#ifndef DISTRIBUTION_API_H
#define DISTRIBUTION_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "pEpEngine.h"
/* Just #include every relevant header for the Distribution protocol family. */
#include "echo_api.h"
#include "key_reset.h"
#ifdef __cplusplus
}
#endif
#endif // #ifndef DISTRIBUTION_API_H

@ -0,0 +1,662 @@
#include "echo_api.h"
#include "pEp_internal.h"
#include "baseprotocol.h"
#include "distribution_codec.h"
#include "sync_api.h" // for notifyHandshake, currently defined in this header
#include <assert.h>
/* Either <sqlite3.h> or "sqlite3.h" has already been included by
pEp_internal.h so we do not need to deal with it here. */
#include "status_to_string.h" // FIXME: remove.
#include "media_key.h" // for identity_known_to_use_pEp
/* Debugging.
* ***************************************************************** */
#define DEBUG_ECHO
#if ! defined(DEBUG_ECHO)
# define echo_log(stream, ...) \
do { /* Do nothing. */ } while (false)
# else
# define echo_log fprintf
#endif
/* Initialisation and finalisation
* ***************************************************************** */
/* The functions in this section actually serve to handle prepared SQL
statements, compiled once and for all from these SQL statements. */
static const char *echo_upgrade_add_echo_challange_field_text
= " ALTER TABLE Identity"
" ADD COLUMN echo_challenge BLOB;";
static const char *echo_get_challenge_text
= " SELECT echo_challenge"
" FROM Identity I"
" WHERE I.address = ?1"
" AND I.user_id = ?2;";
static const char *echo_set_challenge_text
= " UPDATE Identity"
" SET echo_challenge = ?1"
" WHERE address = ?2"
" AND user_id = ?3;";
/* This is a convenient way to check for SQL errors without duplicating code. */
#define ON_SQL_ERROR_SET_STATUS_AND_GOTO \
do { \
if (sql_status != SQLITE_OK \
&& sql_status != SQLITE_DONE \
&& sql_status != SQLITE_ROW) { \
status = PEP_UNKNOWN_DB_ERROR; \
/* This should not happen in production, \
so I can afford a debug print when \
something unexpected happens. */ \
if (sql_status == SQLITE_ERROR) \
fprintf(stderr, "SQL ERROR: %s\n", \
sqlite3_errmsg(session->db)); \
goto end; \
} \
} while (false)
/**
* <!-- upgrade_add_echo_challange_field() -->
*
* @brief Upgrade database schema to support the Echo protocol. Alter the
* identity table to add an echo_challange column, in case it is not
* there already. There is no need to version this simple change.
*
* This is called at initialisation.
*
* @param[in] session
*
* @retval PEP_STATUS_OK upgrade successful or not needed
* @retval PEP_UNKNOWN_DB_ERROR unforeseen database error
*
*/
static PEP_STATUS upgrade_add_echo_challange_field(PEP_SESSION session) {
/* Sanity checks. */
assert(session);
if (! session)
return PEP_ILLEGAL_VALUE;
/* Alter the table. This is executed only once at initialisation time,
so keeping the SQL statement prepared would be counter-productive. */
int sql_status
= sqlite3_exec(session->db,
" ALTER TABLE Identity"
" ADD COLUMN echo_challenge BLOB;"
, NULL, NULL, NULL);
switch (sql_status) {
case SQLITE_OK:
/* Upgrading was successful. */
return PEP_STATUS_OK;
case SQLITE_ERROR:
/* Upgrading was not needed, but this is not a problem: the column
we want to add exists. */
return PEP_STATUS_OK;
default:
/* An actual unforeseen error. */
return PEP_UNKNOWN_DB_ERROR;
}
}
PEP_STATUS echo_initialize(PEP_SESSION session)
{
/* Sanity checks. */
assert(session && session->db);
if (! (session && session->db))
return PEP_ILLEGAL_VALUE;
/* Change the schema if needed, once and for all. We want to do this
*before* prepraring statements using the new column that is created by
this upgrade. */
PEP_STATUS status = PEP_STATUS_OK;
status = upgrade_add_echo_challange_field(session);
if (status != PEP_STATUS_OK)
goto end;
/* Prepare SQL statements, so that we only do it once and for all. This
will be important in the future for embedded platforms with limited
resources. */
int sql_status;
sql_status = sqlite3_prepare_v2(session->db, echo_get_challenge_text,
-1, &session->echo_get_challenge,
NULL);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
sql_status = sqlite3_prepare_v2(session->db, echo_set_challenge_text,
-1, &session->echo_set_challenge,
NULL);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
end:
return status;
}
PEP_STATUS echo_finalize(PEP_SESSION session)
{
/* Sanity checks. */
assert(session);
if (! session)
return PEP_ILLEGAL_VALUE;
/* Finialise prepared SQL statements. */
sqlite3_finalize(session->echo_get_challenge);
sqlite3_finalize(session->echo_set_challenge);
return PEP_STATUS_OK;
}
/* Challenge/response handling
* ***************************************************************** */
/**
* <!-- echo_challenge_for_identity() -->
*
* @brief Retrieve the stored challenge for the given identity; if
* the identity has no stored challenge write a new one first.
* This is inteded for use when:
* (1) preparing a challenge for an outgoing Ping message;
* (2) checking that an incoming Pong message has the repsonse
* we expect.
*
* @param[in] session session
* @param[in] identity the identity we are dealing with
* @param[out] challenge ownership remains to the caller.
* Only meaningful on success.
*
* @retval PEP_STATUS_OK success
* @retval PEP_UNKNOWN_DB_ERROR unforeseen database error
*
*/
static PEP_STATUS echo_challenge_for_identity(PEP_SESSION session,
const pEp_identity *identity,
pEpUUID challenge)
{
/* Sanity checks. */
assert(session && identity && challenge);
if (! (session && identity && challenge))
return PEP_ILLEGAL_VALUE;
/* I need this below, in some cases. It is easier to declare it here and to
set it to NULL, in order to free it unconditionally at the end. */
pEp_identity *identity_copy = NULL;
/* Define a macro used everywhere with the SQL api below. */
PEP_STATUS status = PEP_STATUS_OK;
int sql_status;
/* Look at the database. First check if we have a stored challenge... */
reset_and_clear_bindings(session->echo_get_challenge);
sql_status = sqlite3_bind_text(session->echo_get_challenge,
1, identity->address, -1, SQLITE_STATIC);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
sql_status = sqlite3_bind_text(session->echo_get_challenge,
2, identity->user_id, -1, SQLITE_STATIC);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
sql_status = sqlite3_step(session->echo_get_challenge);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
const void *stored_challenge;
if (sql_status != SQLITE_ROW) {
/* The identity is not in the database yet: make sure it is there before
we alter the row in order to set its challenge field.
FIXME FIXME FIXME: this is not supposed to happen, but it does at
least on 2.x. There should be no need to call update_identity
if this is called from decrypt_message or outgoing_message_rating or
outgoing_message_rating_preview .
Something in 2.x is badly broken, and not worth fixing. In 3.x we
shall find the actual problem. */
identity_copy = identity_dup(identity);
if (identity_copy == NULL) {
status = PEP_OUT_OF_MEMORY;
goto end;
}
status = update_identity(session, identity_copy);
if (status != PEP_STATUS_OK)
goto end;
stored_challenge = NULL;
}
else if (sqlite3_column_type(session->echo_get_challenge, 0) == SQLITE_NULL)
stored_challenge = NULL;
else
stored_challenge = sqlite3_column_blob(session->echo_get_challenge, 0);
if (stored_challenge != NULL) {
memcpy(challenge, stored_challenge, sizeof(pEpUUID));
goto end;
}
/* If we are here then we have no stored challenge. Make a new one... */
uuid_generate_random(challenge);
/* These crude alternatives are convenient for debugging: */
//challenge[sizeof(pEpUUID) - 1] = '\0';
//sprintf(challenge, "to-%s-%i", getenv("USER"), rand() % (1 << 15));
/* ...and store it into the database. */
reset_and_clear_bindings(session->echo_set_challenge);
sql_status
= sqlite3_bind_blob(session->echo_set_challenge,
1, challenge, sizeof(pEpUUID), SQLITE_STATIC);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
sql_status
= sqlite3_bind_text(session->echo_set_challenge, 2, identity->address,
-1, SQLITE_STATIC);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
sql_status
= sqlite3_bind_text(session->echo_set_challenge, 3, identity->user_id,
-1, SQLITE_STATIC);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
sql_status = sqlite3_step(session->echo_set_challenge);
ON_SQL_ERROR_SET_STATUS_AND_GOTO;
/* If we arrived here then the SQL UPDATE statement succeeded. */
end:
free_identity(identity_copy);
return status;
}
PEP_STATUS handle_pong(PEP_SESSION session,
const pEp_identity *own_identity,
const pEp_identity *partner_identity,
const Distribution_t *pong_distribution_message)
{
/* Sanity checks. */
if (! (session && own_identity && partner_identity
&& pong_distribution_message))
return PEP_ILLEGAL_VALUE;
if (pong_distribution_message->present != Distribution_PR_echo)
return PEP_ILLEGAL_VALUE;
if (pong_distribution_message->choice.echo.present != Echo_PR_echoPong)
return PEP_ILLEGAL_VALUE; /* We handle Pong, not Ping. */
/* Retrieve the two values. */
PEP_STATUS status = PEP_STATUS_OK;
pEpUUID expected_response;
status = echo_challenge_for_identity(session, partner_identity,
expected_response);
if (status != PEP_STATUS_OK)
return status;
pEpUUID actual_response; /* I am not completely sure about how the type is
defined on windows: make this robust at the cost
of one more copy. */
memcpy(actual_response,
pong_distribution_message->choice.echo.choice.echoPong.challenge.buf,
pong_distribution_message->choice.echo.choice.echoPong.challenge.size);
/* Compare the response to the stored challenge. */
if (memcmp(actual_response, expected_response, sizeof(pEpUUID))) {
/* Bad repsonse! It is different from the stored challenge. */
return PEP_DISTRIBUTION_ILLEGAL_MESSAGE;
}
else {
/* Good response. Okay, notify the application that some rating
might have improved. */
fprintf(stderr, "session->notifyHandshake is %p\n", session->notifyHandshake);
if (session->notifyHandshake == NULL)
return PEP_SYNC_NO_NOTIFY_CALLBACK;
pEp_identity *own_identity_copy = identity_dup(own_identity);
pEp_identity *partner_identity_copy
= identity_dup(partner_identity);
if (own_identity_copy != NULL || partner_identity_copy != NULL)
goto fail;
echo_log(stderr, "SYNC_NOTIFY_OUTGOING_RATING_CHANGE\n");
return session->notifyHandshake(own_identity_copy,
partner_identity_copy,
SYNC_NOTIFY_OUTGOING_RATING_CHANGE);
fail:
free(own_identity_copy);
free(partner_identity_copy);
return PEP_OUT_OF_MEMORY;
}
}
/* Echo messages
* ***************************************************************** */
/* Return a new Ping or Pong message, or NULL on failure. The given uuid is
used to fill the challenge / response field. */
static Distribution_t* create_Ping_or_Pong_message(const pEpUUID uuid,
bool ping)
{
Distribution_t *msg = calloc(sizeof(Distribution_t), 1);
if (msg == NULL || uuid == NULL)
return NULL;
msg->present = Distribution_PR_echo;
int failure;
if (ping) {
msg->choice.echo.present = Echo_PR_echoPing;
failure = OCTET_STRING_fromBuf(& msg->choice.echo.choice.echoPing.challenge,
(char *) uuid, 16);
}
else {
msg->choice.echo.present = Echo_PR_echoPong;
failure = OCTET_STRING_fromBuf(& msg->choice.echo.choice.echoPong.challenge,
(char *) uuid, 16);
}
if (failure) {
free(msg);
return NULL;
}
return msg;
}
/* A helper factoring the common code in send_ping and send_pong.
The Boolean flag determines what kind of message is sent. The uuid field
is used for challenge / response. */
static PEP_STATUS send_ping_or_pong(PEP_SESSION session,
const pEp_identity *from,
const pEp_identity *to,
const pEpUUID uuid,
bool ping)
{
/* Sanity checks. */
if (! (session && session->messageToSend && from && to))
return PEP_ILLEGAL_VALUE;
if (! session->enable_echo_protocol) {
fprintf(stderr, "* Echo protocol disabled: not sending a %s to %s <%s>\n", (ping ? "Ping" : "Pong"), (to->username ? to->username : "<no username>"), (to->address ? to->address : "<no address>"));
return PEP_STATUS_OK;
}
PEP_STATUS status = PEP_STATUS_OK;
char *data = NULL;
/* Craft an attachment. */
Distribution_t *msg = create_Ping_or_Pong_message(uuid, ping);
if (msg == NULL)
return PEP_OUT_OF_MEMORY;
/* Encode it as an ASN.1 PER, then free the one we built. */
size_t size;
status = encode_Distribution_message(msg, &data, &size);
ASN_STRUCT_FREE(asn_DEF_Distribution, msg); /* free on error as well:
move sementics */
if (status != PEP_STATUS_OK)
return PEP_OUT_OF_MEMORY;
/* Make a message with the binary attached, as a network-data-structure
message. */
message *non_encrypted_m = NULL;
status = base_prepare_message(session, from, to, BASE_DISTRIBUTION,
data, size, NULL, & non_encrypted_m);
if (status != PEP_STATUS_OK) {
free(data);
return status;
}
/* "Encrypt" the message in the sense of calling encrypt_message; this, in
case we have no key for the recipient, as it will normally happen with
Ping messages, will alter the message to contain the sender's key. */
message *m = NULL;
status = encrypt_message(session, non_encrypted_m, NULL, &m,
PEP_enc_PEP, PEP_encrypt_flag_default);
echo_log(stderr, " send %s from %s <%s> to %s <%s>, status after encrypting %i %s\n", (ping ? "Ping" : "Pong"), from->username, from->address, to->username, to->address, status, pEp_status_to_string(status));
if (status == PEP_STATUS_OK)
free_message(non_encrypted_m);
else if (status == PEP_UNENCRYPTED)
m = non_encrypted_m;
else {
free_message(non_encrypted_m);
/* Differently from a status of PEP_UNENCRYPTED this is an actual
unexpected error, to be reported to the caller. */
return status;
}
/* Send it. */
status = session->messageToSend(m);
if (status != PEP_STATUS_OK) {
free_message(m);
return status;
}
/* In case of success we must *not* free the message: the called function
gets ownership of it. */
return PEP_STATUS_OK;
}
PEP_STATUS send_ping(PEP_SESSION session,
const pEp_identity *from,
const pEp_identity *to)
{
pEpUUID challenge;
PEP_STATUS status = echo_challenge_for_identity(session, to, challenge);
if (status != PEP_STATUS_OK)
return status;
else
return send_ping_or_pong(session, from, to, challenge, true);
}
PEP_STATUS send_pong(PEP_SESSION session,
const message *ping_message,
const Distribution_t *ping_distribution_message) {
fprintf(stderr, "WWWW session->notifyHandshake is %p\n", session->notifyHandshake);
/* Argument checks. No need to check for messageToSend here, since we
will check later when actually sending. */
assert(session && ping_message && ping_distribution_message);
if (! (session && ping_message && ping_distribution_message))
return PEP_ILLEGAL_VALUE;
/* Sanity checks. */
if (ping_message->dir != PEP_dir_incoming)
return PEP_ILLEGAL_VALUE;
if (ping_message->recv_by == NULL)
return PEP_ILLEGAL_VALUE;
if (! ping_message->recv_by->me)
return PEP_ILLEGAL_VALUE;
if (ping_distribution_message->present != Distribution_PR_echo)
return PEP_ILLEGAL_VALUE;
if (ping_distribution_message->choice.echo.present != Echo_PR_echoPing)
return PEP_ILLEGAL_VALUE; /* We reply to Ping, not to Pong. */
/* About identities, the To and From fields must be swapped between ping and
pong. In particular we have that pong.from = ping.recv_by
and that pong.to = ping.from .
About the challenge, we simply reuse the challenge allocated string
as a response. */
const pEp_identity *pong_from = ping_message->recv_by;
if (! pong_from->me)
return PEP_ILLEGAL_VALUE;
const pEp_identity *pong_to = ping_message->from;
const unsigned char *response
= ping_distribution_message->choice.echo.choice.echoPing.challenge.buf;
return send_ping_or_pong(session,
pong_from,
pong_to,
response,
false);
}
/* Policy
* ***************************************************************** */
/* The functions in this section serve to implement some policy using the
Distribution.Echo protocol.
Properly handling failure in a situation where we send multiple messages
to multiple recipients over an unreliable protocol seems futile; I have
avoided complicated status code returns. */
/* Return true iff the given identity is known, in the sense that we do have at
least a key for it. In case of error consider the identity as known, which
will avoid a Ping. */
static bool identity_known(PEP_SESSION session,
const pEp_identity *identity)
{
bool result = true;
stringlist_t *keys = NULL;
if (identity->me)
return true;
pEp_identity *identity_copy = NULL;
PEP_STATUS status;
status = get_identity(session, identity->address, identity->user_id,
& identity_copy);
if (status != PEP_STATUS_OK) {
/* An identity not in the database is of course not known. */
result = false;
goto end;
}
status = get_all_keys_for_identity(session, identity_copy, &keys);
if (status == PEP_KEY_NOT_FOUND)
result = false;
else if (status == PEP_STATUS_OK)
result = (keys != NULL); /* I could say have written "result = true;"
but I am not fond as PEP_KEY_NOT_FOUND as a
status in this case, and this code will
break if the status is removed later. */
else /* An actual error. */
goto end;
end:
free_identity(identity_copy);
free_stringlist(keys);
return result;
}
/* Send a Distribution.Ping message from the identity to the to identity, if we
do not have a key for the to identity and the identity is not own; do nothing
otherwise. Ignore failures. The to identity is allowed to be NULL.
Iff only_if_pEp is true, do not send Ping messages to identities not known
to use pEp. */
static void send_ping_if_unknown(PEP_SESSION session,
const pEp_identity *from_identity,
const pEp_identity *to_identity,
bool only_if_pEp)
{
assert(session && from_identity);
if (! (session && from_identity))
return;
if (! from_identity->me) {
echo_log(stderr, "send_ping_if_unknown: trying to send from non-own identity %s <%s>\n", from_identity->username, from_identity->address);
return;
}
/* The To identity is allowed to be NULL, but in that case we do nothing.
Own identities are dealt with in identity_known . */
if (to_identity == NULL)
return;
/* In case the identity is unknown we may want to ping it... */
if (! identity_known(session, to_identity))
{
/* ...As long as it uses pEp, or we do not care whether it does. */
if (! only_if_pEp)
send_ping(session, from_identity, to_identity);
else {
bool known_to_use_pEp;
PEP_STATUS status = identity_known_to_use_pEp (session, to_identity,
& known_to_use_pEp);
if (status != PEP_STATUS_OK) {
echo_log(stderr, "!!!! send_ping_if_unknown: %s -> %s FAILED: status %i %s\n", from_identity->address, to_identity->address, (int)status,pEp_status_to_string(status));
return;
}
if (known_to_use_pEp)
send_ping(session, from_identity, to_identity);
}
}
}
/* Send a Distribution.Ping message from the from identity to every identity in
the to list which has no known key. Ignore failures. If only_pEp is true
ignore identities not known to use pEp. */
static void send_ping_to_unknowns_in(PEP_SESSION session,
const pEp_identity *from_identity,
const identity_list *to_identities,
bool only_pEp)
{
const identity_list *rest;
for (rest = to_identities; rest != NULL; rest = rest->next)
send_ping_if_unknown(session, from_identity, rest->ident, only_pEp);
}
/* This factors the common logic of
send_ping_to_all_unknowns_in_incoming_message and
send_ping_to_unknown_pEp_identities_in_incoming_message . */
static PEP_STATUS send_ping_to_unknowns_in_incoming_message(PEP_SESSION session,
const message *msg,
bool only_pEp)
{
/* Sanity checks. */
assert(session && msg);
if (! (session && msg))
return PEP_ILLEGAL_VALUE;
if (msg->dir != PEP_dir_incoming)
return PEP_ILLEGAL_VALUE;
/* Find the identity who received the message and should send Pings. */
const pEp_identity *ping_from_identity = msg->recv_by;
if (msg->recv_by == NULL) {
/* Applications are supposed never to let this happen, but in practice
it is difficult to find a reasonable value for messages received as
Bcc. */
fprintf(stderr, "APPLICATION BUG: message %s \"%s\" has no Recv-By\n", msg->id, msg->shortmsg ? msg->shortmsg : "<no subject>");
return PEP_ILLEGAL_VALUE;
}
/* Send Pings. It is harmless to consider our own identities as well as
potential Ping recipients: those will simply never be sent to, as they
will all have a known key. Here we do not make any effort to avoid
sending multiple Ping messages to the same recipient. */
send_ping_if_unknown(session, ping_from_identity, msg->from, only_pEp);
send_ping_to_unknowns_in(session, ping_from_identity, msg->to, only_pEp);
send_ping_to_unknowns_in(session, ping_from_identity, msg->cc, only_pEp);
send_ping_to_unknowns_in(session, ping_from_identity, msg->reply_to,
only_pEp);
/* Do not consider Bcc identities; the Bcc field should be empty anyway,
and sending Pings would leak privacy. */
return PEP_STATUS_OK;
}
PEP_STATUS send_ping_to_all_unknowns_in_incoming_message(PEP_SESSION session,
const message *msg)
{
//echo_log(stderr, "send_ping_to_all_unknowns_in_incoming_message\n");
return send_ping_to_unknowns_in_incoming_message (session, msg, false);
}
PEP_STATUS send_ping_to_unknown_pEp_identities_in_incoming_message(PEP_SESSION session,
const message *msg)
{
//echo_log(stderr, "send_ping_to_unknown_pEp_identities_in_incoming_message\n");
return send_ping_to_unknowns_in_incoming_message (session, msg, true);
}
PEP_STATUS send_ping_to_unknown_pEp_identities_in_outgoing_message(PEP_SESSION session,
const message *msg)
{
//echo_log(stderr, "send_ping_to_unknown_pEp_identities_in_outgoing_message\n");
/* Sanity checks. */
assert(session && msg);
if (! (session && msg))
return PEP_ILLEGAL_VALUE;
if (msg->dir != PEP_dir_outgoing)
return PEP_ILLEGAL_VALUE;
/* Find the identity who is sending the message and should send Pings. */
const pEp_identity *ping_from_identity = msg->from;
if (msg->from == NULL) {
fprintf(stderr, "message %s \"%s\" has no From\n", msg->id, msg->shortmsg ? msg->shortmsg : "<no subject>");
return PEP_ILLEGAL_VALUE;
}
/* Send Pings to identities known to use pEp -- see the Boolean parameter at
the end. It is harmless to consider our own identities as well as
potential Ping recipients: those will simply never be sent to, as they
will all have a known key. Here we do not make any effort to avoid
sending multiple Ping messages to the same recipient. */
send_ping_to_unknowns_in(session, ping_from_identity, msg->to, true);
send_ping_to_unknowns_in(session, ping_from_identity, msg->cc, true);
send_ping_to_unknowns_in(session, ping_from_identity, msg->reply_to, true);
/* Do not consider Bcc identities; the Bcc field should be empty anyway,
and sending Pings would leak privacy. */
return PEP_STATUS_OK;
}

@ -0,0 +1,197 @@
/**
* @file echo_api.h
* @brief echo API
* @license GNU General Public License 3.0 - see LICENSE.txt
*/
#ifndef ECHO_API_H
#define ECHO_API_H
#include "pEpEngine.h"
#include "pEp_internal.h" // for message
#include "Distribution.h" // for Distribution_t
#ifdef __cplusplus
extern "C" {
#endif
/* Initialisation and finalisation.
* ***************************************************************** */
/* The functions here are called when a session is initialised or finalised. */
/**
* <!-- echo_initialize() -->
*
* @brief Initialize session-wide support for the Echo protocol.
* This is called at initialisation, *after* the DB subsystem
* has alraedy been initialised.
*
* @param[in] session session
*
* @retval PEP_STATUS_OK success
* @retval PEP_ILLEGAL_VALUE NULL session or db within session
* @retval PEP_UNKNOWN_DB_ERROR database error
*
*/
PEP_STATUS echo_initialize(PEP_SESSION session);
/**
* <!-- echo_finalise() -->
*
* @brief Finalise session-wide support for the Echo protocol.
*
* @param[in] session session
*
* @retval PEP_STATUS_OK success
* @retval PEP_ILLEGAL_VALUE NULL session
*
*/
PEP_STATUS echo_finalize(PEP_SESSION session);
/* Sending Ping and Pong messages.
* ***************************************************************** */
/**
* <!-- send_ping() -->
*
* @brief Send a ping message from the given from identity, which must be own,
* to the given to identity.
*
* @param[in] session session
* @param[in] from sender identity, must be own
* @param[in] to recipient identity
*
* @retval PEP_STATUS_OK messageToSend returned with success
* @retval PEP_ILLEGAL_VALUE session, from, to or messageToSend not
* defined
* @retval PEP_OUT_OF_MEMORY out of memory
* @retval any other error status
* returned by messageToSend
*
* @note This automatically builds a message and sends it by calling
* messageToSend on it; if messageToSend fails then its return
* status is returned to the caller.
*
*/
PEP_STATUS send_ping(PEP_SESSION session,
const pEp_identity *from,
const pEp_identity *to);
/**
* <!-- send_pong() -->
*
* @brief Send a Pong message in response to the given Ping message, which
* must have been sent to an own identity.
*
* @param[in] ping_message the message we are replying to: this
* will always be already decrypted;
* @param[in] ping_distribution_message
* the Distribution_t message encoded as one of
* the attechments of ping_message;
*
* @retval PEP_STATUS_OK messageToSend returned with success
* @retval PEP_ILLEGAL_VALUE session, ping_message,
* ping_distribution_message or messageToSend
* not defined, ping_distribution_message
* not actually a Ping message.
* @retval PEP_OUT_OF_MEMORY out of memory
* @retval any other error status returned by messageToSend
*
*/
PEP_STATUS send_pong(PEP_SESSION session,
const message *ping_message,
const Distribution_t *ping_distribution_message);
/**
* <!-- handle_pong() -->
*
* @brief Handle a received Pong message; check the challenge and, in case
* of success, send a SYNC_NOTIFY_OUTGOING_RATING_CHANGE notification.
*
* @param[in] session session
* @param[in] own_identity the identity which received the Pong message
* @param[in] partner_identity the communication partner we are dealing with
* @param[in] pong_distribution_message
* the Pong message
*
* @retval PEP_STATUS_OK success: response matches stored challenge,
* notification sent.
* @retval PEP_DISTRIBUTION_ILLEGAL_MESSAGE
* mismatching reponse
* @retval PEP_SYNC_NO_NOTIFY_CALLBACK
* no notifyCallback, but correct reponse
* @retval PEP_ILLEGAL_VALUE any argument NULL, message not a Pong
* @retval PEP_OUT_OF_MEMORY cannot allocate memory
* @retval PEP_UNKNOWN_DB_ERROR unforeseen database error
* @retval any other error code relayed from
* notifyChallenge
*
*/
PEP_STATUS handle_pong(PEP_SESSION session,
const pEp_identity *own_identity,
const pEp_identity *partner_identity,
const Distribution_t *pong_distribution_message);
/**
* <!-- send_ping_to_all_unknowns_in_incoming_message() -->
*
* @brief Send a Distribution.Ping message (ignoring failures) from the given
* incoming message's Recv-by identity to any identity for which we have
* no key mentioned as a non-Bcc recipeint or as a Reply-To.
* Notice that this should not be called blindly on any random message,
* as non-pEp-users would be annoyed to get pEp administrative messages.
* This in practice should be called when the message rating is at least
* PEP_rating_reliable (yellow).
*
* @param[in] session session
* @param[in] msg the message containing potentially unknown
* identities as fields
*
* @retval PEP_STATUS_OK success
* @retval PEP_ILLEGAL_VALUE any argument NULL, non-incoming message,
* no Recv-by (which applications are not
* supposed to allow, but still
* happens in practice)
*
*/
PEP_STATUS send_ping_to_all_unknowns_in_incoming_message(PEP_SESSION session,
const message *msg);
/**
* <!-- send_ping_to_unknown_pEp_identities_in_incoming_message() -->
*
* @brief Exactly like send_ping_to_all_unknowns_in_incoming_message , with
* the difference that this function only sends messages to identities
* known to use pEp.
* Rationale: an identity may be known to use pEp even if we do not
* know anything about it, thanks to media keys.
*/
PEP_STATUS send_ping_to_unknown_pEp_identities_in_incoming_message(PEP_SESSION session,
const message *msg);
/**
* <!-- send_ping_to_unknown_pEp_identities_in_outgoing_message() -->
*
* @brief Like send_ping_to_unknown_pEp_identities_in_incoming_message ,
* for outgoing messages.
* Rationale: an identity may be known to use pEp even if we do not
* know anything about it, thanks to media keys.
* This is useful when composing a message: we might be
* able to receive a Pong, and therefore a recipient key,
* even before sending; this is a way to improve the
* outgoing message rating.
*/
PEP_STATUS send_ping_to_unknown_pEp_identities_in_outgoing_message(PEP_SESSION session,
const message *msg);
#ifdef __cplusplus
}
#endif
#endif // #ifndef ECHO_API_H

@ -17,6 +17,7 @@
#include "keymanagement.h"
#include "keymanagement_internal.h"
#include "KeySync_fsm.h"
#include "media_key.h"
static bool key_matches_address(PEP_SESSION session, const char* address,
const char* fpr) {
@ -882,6 +883,9 @@ DYNAMIC_API PEP_STATUS update_identity(
identity->comm_type = PEP_ct_key_not_found;
}
// Update with media key information.
status = amend_identity_with_media_key_information(session, identity);
goto pEp_free;
enomem:

@ -0,0 +1,497 @@
#include "media_key.h"
#include "pEp_internal.h"
#include "stringpair.h" // for stringpair_list_t
#include <assert.h>
#include <string.h>
/* Debugging.
* ***************************************************************** */
//#define DEBUG_MEDIA_KEY
#if ! defined(DEBUG_MEDIA_KEY)
# define fprintf(stream, ...) \
do { /* Do nothing. */ } while (false)
#endif
/* Initialisation and finalisation.
* ***************************************************************** */
PEP_STATUS media_key_finalize_map(PEP_SESSION session) {
/* Sanity checks. */
assert(session);
if(session == NULL)
return PEP_ILLEGAL_VALUE;
/* Do the work. */
free_stringpair_list(session->media_key_map);
/* Out of defensiveness. */
session->media_key_map = NULL;
return PEP_STATUS_OK;
}
/* Utility.
* ***************************************************************** */
static bool fprs_equal(const char *fpr_a, const char *fpr_b)
{
return strcmp(fpr_a, fpr_b) == 0;
}
/* Given a non-NULL '\0'-terminated string return either the same string or a
substring of it without the initial "mailto:" prefix, if such prefix is
present. This function performs no allocation or copy: the result is always
a substring of the argument. */
static const char *normalize_address(const char *address) {
if (strstr(address, "mailto:") == NULL)
return address;
else
return address + /* strlen("mailto:") */ 7;
}
/* Configuration.
* ***************************************************************** */
/* Internally I make sure the map is represented as a "normal-form" list,
without silly NULL value fields; to make sure the map is well-built I build
it myself, using media_key_insert internally. */
PEP_STATUS media_key_insert(PEP_SESSION session,
const char *address_pattern,
const char *fpr)
{
/* Sanity checks. */
assert(session && address_pattern && fpr);
if (! (session && address_pattern && fpr))
return PEP_ILLEGAL_VALUE;
/* Work with a normalised version of the address pattern. */
address_pattern = normalize_address(address_pattern);
stringpair_list_t *old_map = session->media_key_map;
stringpair_t *new_pair = NULL;
new_pair = new_stringpair(address_pattern, fpr);
if (new_pair == NULL)
goto out_of_memory;
stringpair_list_t *new_last_element
= stringpair_list_add(old_map, new_pair);
if (new_last_element == NULL)
goto out_of_memory;
/* Else the structured ponted by old_map is modified destructively, so we
have nothing else to do as long as the map was not previously NULL... */
if (old_map == NULL)
session->media_key_map = new_last_element;
return PEP_STATUS_OK;
out_of_memory:
free(new_pair);
return PEP_OUT_OF_MEMORY;
}
PEP_STATUS media_key_remove(PEP_SESSION session,
const char *address_pattern)
{
/* Sanity checks. */
assert(session && address_pattern);
if (! (session && address_pattern))
return PEP_ILLEGAL_VALUE;
/* Work with a normalised version of the address pattern, so that we can
abstract from "mailto:" prefixes. The stored address patterns have been
normalised already. */
address_pattern = normalize_address(address_pattern);
/* Do a linear scan, keeping a pointer to the previous node so that we can
modify the pointer to the current element when we find a match. */
stringpair_list_t **previous = & session->media_key_map;
stringpair_list_t *rest = session->media_key_map;
while (rest != NULL) {
const char *item_address_pattern = rest->value->key;
if (! strcmp (address_pattern, item_address_pattern)) {
free (rest->value->key);
free (rest->value->value);
free (rest->value);
* previous = rest->next;
free (rest);
return PEP_STATUS_OK;
}
previous = & rest->next;
rest = rest->next;
}
/* If we reached the end then there was no match. */
return PEP_KEY_NOT_FOUND;
}
/* The public function */
DYNAMIC_API PEP_STATUS config_media_keys(PEP_SESSION session,
const stringpair_list_t *new_map)
{
/* Sanity checks. */
assert(session);
if (! (session))
return PEP_ILLEGAL_VALUE;
/* A more complicated sanity check: that the new map is well formed. */
const stringpair_list_t *rest;
for (rest = new_map; rest != NULL; rest = rest->next)
{
const stringpair_t *pair = rest->value;
/* Null pairs in a list of pairs are silly, but I can very easily
accept them anyway. */
if (pair == NULL)
continue;
const char *item_address_pattern = pair->key;
const char *item_fpr = pair->value;
if (item_address_pattern == NULL
|| item_fpr == NULL)
return PEP_ILLEGAL_VALUE;
}
/* Keep the old map, to be restored in case of any error. */
stringpair_list_t *old_map = session->media_key_map;
/* Do the work: add every pair. */
PEP_STATUS status = PEP_STATUS_OK;
for (rest = new_map; rest != NULL; rest = rest->next) {
const stringpair_t *pair = rest->value;
const char *item_address_pattern = pair->key;
const char *item_fpr = pair->value;
status = media_key_insert(session, item_address_pattern, item_fpr);
if (status != PEP_STATUS_OK)
goto end;
}
end:
if (status != PEP_STATUS_OK)
session->media_key_map = old_map;
else
free_stringpair_list(old_map);
return status;
}
/* Lookup.
* ***************************************************************** */
PEP_STATUS media_key_lookup_address(PEP_SESSION session,
const char *address,
char **fpr_result)
{
/* Sanity checks. */
assert(session && address && fpr_result);
if (! (session && address && fpr_result))
return PEP_ILLEGAL_VALUE;
/* Use a normalised version of the address; it is better to do this once and
for all, out of the loop. Notice that the address patterns being tested
in the loop are already normalised. */
address = normalize_address(address);
/* Perform a trivial linear search on the list, with the first match
winning. */
const stringpair_list_t *rest;
for (rest = session->media_key_map; rest != NULL; rest = rest->next) {
const char *item_address_pattern = rest->value->key;
const char *item_fpr = rest->value->value;
//fprintf(stderr, "* check: <%s, %s>\n", item_address_pattern, item_fpr);
if (! pEp_fnmatch(item_address_pattern, address)) {
*fpr_result = strdup(item_fpr);
if (*fpr_result == NULL)
return PEP_OUT_OF_MEMORY;
else {
fprintf(stderr, "<%s>: media key %s, matching pattern %s\n", address, *fpr_result, item_address_pattern);
return PEP_STATUS_OK;
}
}
}
/* If we arrived here there is no match. Set the output parameter as well,
just for defensiveness' sake. */
*fpr_result = NULL;
return PEP_KEY_NOT_FOUND;
}
PEP_STATUS media_key_has_identity_a_media_key(PEP_SESSION session,
const pEp_identity *identity,
bool *has_media_key)
{
/* Sanity checks. */
assert(session && identity && has_media_key);
if (! (session && identity && has_media_key))
return PEP_ILLEGAL_VALUE;
char *media_key = NULL;
PEP_STATUS status
= media_key_lookup_address(session, identity->address, & media_key);
free(media_key);
switch (status) {
case PEP_STATUS_OK:
* has_media_key = true;
break;
case PEP_KEY_NOT_FOUND:
* has_media_key = false;
status = PEP_STATUS_OK;
break;
default:
/* We would not need to do anything: the status is already what we want
to return. However, out of defensiveness, set the Boolean result
just in case it is used by mistake. In doubt it is better to assume
that an identity has *no* media key. */
* has_media_key = false;
}
return status;
}
/* Media-key-related helpers for other subsystems.
* ***************************************************************** */
PEP_STATUS identity_known_to_use_pEp(PEP_SESSION session,
const pEp_identity *identity,
bool *known_to_use_pEp)
{
/* Sanity checks. */
assert(session && identity && known_to_use_pEp);
if (! (session && identity && known_to_use_pEp))
return PEP_ILLEGAL_VALUE;
bool result = false;
PEP_STATUS status = PEP_STATUS_OK;
pEp_identity *identity_copy = NULL;
/* Easy case: an own identity. */
if (identity->me) {
result = true;
goto end;
}
/* Check the database for major_ver; in case we do not know the identity
yet, check whether the identity has a known media key: any identity with
a media kay is also a pEp-using identity. */
const pEp_identity *identity_to_check_and_not_free;
status = get_identity(session, identity->address, identity->user_id,
& identity_copy);