Compare commits
1 Commits
6dd1610e47
...
fix/data-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
b63a9d29f8
|
@@ -236,7 +236,10 @@ omemo_sources = \
|
||||
src/tools/aesgcm_download.h src/tools/aesgcm_download.c
|
||||
|
||||
omemo_unittest_sources = \
|
||||
tests/unittests/omemo/stub_omemo.c
|
||||
tests/unittests/omemo/stub_omemo.c \
|
||||
tests/unittests/omemo/test_omemo_crypto.c \
|
||||
tests/unittests/omemo/test_omemo_crypto.h \
|
||||
src/omemo/crypto.c
|
||||
|
||||
if BUILD_PYTHON_API
|
||||
core_sources += $(python_sources)
|
||||
|
||||
@@ -342,12 +342,6 @@ AS_IF([test "x$enable_gdk_pixbuf" != xno],
|
||||
[AC_MSG_ERROR([gdk-pixbuf-2.0 >= 2.4 is required to scale avatars before uploading])],
|
||||
[AC_MSG_NOTICE([gdk-pixbuf-2.0 >= 2.4 not found, GDK Pixbuf support not enabled])])])])
|
||||
|
||||
dnl libgcrypt: CSPRNG for stanza ids and instance identifiers (also pulled in by OMEMO/OTR)
|
||||
AC_CHECK_LIB([gcrypt], [gcry_create_nonce],
|
||||
[AC_DEFINE([HAVE_LIBGCRYPT], [1], [Have libgcrypt])
|
||||
LIBS="-lgcrypt $LIBS"],
|
||||
[AC_MSG_NOTICE([libgcrypt not found, identifiers will use the OS random device])])
|
||||
|
||||
dnl feature: omemo
|
||||
AM_CONDITIONAL([BUILD_OMEMO], [false])
|
||||
if test "x$enable_omemo" != xno; then
|
||||
|
||||
@@ -9558,11 +9558,6 @@ _url_aesgcm_method(ProfWin* window, const char* cmd_template, gchar* url, gchar*
|
||||
return;
|
||||
AESGCMDownload* download = g_new0(AESGCMDownload, 1);
|
||||
download->id = get_random_string(4);
|
||||
if (!download->id) {
|
||||
cons_show_error("Could not start download: no random source available.");
|
||||
g_free(download);
|
||||
return;
|
||||
}
|
||||
download->url = strdup(url);
|
||||
download->filename = strdup(filename);
|
||||
if (cmd_template != NULL) {
|
||||
@@ -9585,11 +9580,6 @@ _download_install_plugin(ProfWin* window, gchar* url, gchar* path)
|
||||
return FALSE;
|
||||
HTTPDownload* download = g_new0(HTTPDownload, 1);
|
||||
download->id = get_random_string(4);
|
||||
if (!download->id) {
|
||||
cons_show_error("Could not start download: no random source available.");
|
||||
g_free(download);
|
||||
return FALSE;
|
||||
}
|
||||
download->url = strdup(url);
|
||||
download->filename = strdup(filename);
|
||||
download->cmd_template = NULL;
|
||||
@@ -9609,11 +9599,6 @@ _url_http_method(ProfWin* window, const char* cmd_template, gchar* url, gchar* p
|
||||
return;
|
||||
HTTPDownload* download = g_new0(HTTPDownload, 1);
|
||||
download->id = get_random_string(4);
|
||||
if (!download->id) {
|
||||
cons_show_error("Could not start download: no random source available.");
|
||||
g_free(download);
|
||||
return;
|
||||
}
|
||||
download->url = strdup(url);
|
||||
download->filename = strdup(filename);
|
||||
download->cmd_template = cmd_template ? strdup(cmd_template) : NULL;
|
||||
|
||||
146
src/common.c
146
src/common.c
@@ -23,9 +23,6 @@
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <curl/easy.h>
|
||||
#ifdef HAVE_LIBGCRYPT
|
||||
#include <gcrypt.h>
|
||||
#endif
|
||||
#include <glib.h>
|
||||
#include <gio/gio.h>
|
||||
#include <glib/gstdio.h>
|
||||
@@ -458,6 +455,32 @@ str_xml_sanitize(const char* const str)
|
||||
return g_string_free(sanitized, FALSE);
|
||||
}
|
||||
|
||||
gchar*
|
||||
redact_secrets(const char* const str)
|
||||
{
|
||||
if (str == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// SASL exchanges and <password> elements carry credentials — strip their content before logging
|
||||
static gsize init = 0;
|
||||
static GRegex* secret_regex = NULL;
|
||||
if (g_once_init_enter(&init)) {
|
||||
secret_regex = g_regex_new(
|
||||
"(<(?:auth|response|challenge|success|password|digest)\\b[^>]*>)[^<]+(</(?:auth|response|challenge|success|password|digest)>)",
|
||||
0, 0, NULL);
|
||||
g_once_init_leave(&init, 1);
|
||||
}
|
||||
|
||||
if (secret_regex == NULL) {
|
||||
return g_strdup(str);
|
||||
}
|
||||
|
||||
auto_gchar gchar* valid = g_utf8_make_valid(str, -1); // invalid UTF-8 would make the regex fail open
|
||||
gchar* redacted = g_regex_replace(secret_regex, valid, -1, 0, "\\1[REDACTED]\\2", 0, NULL);
|
||||
return redacted ? redacted : g_steal_pointer(&valid);
|
||||
}
|
||||
|
||||
char*
|
||||
release_get_latest(void)
|
||||
{
|
||||
@@ -473,26 +496,9 @@ release_get_latest(void)
|
||||
curl_easy_setopt(handle, CURLOPT_TIMEOUT, 2L);
|
||||
curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void*)&output);
|
||||
|
||||
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYPEER, 1L);
|
||||
curl_easy_setopt(handle, CURLOPT_SSL_VERIFYHOST, 2L);
|
||||
curl_easy_setopt(handle, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
#if LIBCURL_VERSION_NUM >= 0x075500 // 7.85.0 introduced the string form
|
||||
curl_easy_setopt(handle, CURLOPT_PROTOCOLS_STR, "https");
|
||||
curl_easy_setopt(handle, CURLOPT_REDIR_PROTOCOLS_STR, "https");
|
||||
#else
|
||||
curl_easy_setopt(handle, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
|
||||
curl_easy_setopt(handle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
|
||||
#endif
|
||||
|
||||
CURLcode res = curl_easy_perform(handle);
|
||||
curl_easy_perform(handle);
|
||||
curl_easy_cleanup(handle);
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
log_warning("Update check failed: %s", curl_easy_strerror(res));
|
||||
free(output.buffer);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (output.buffer) {
|
||||
output.buffer[output.size++] = '\0';
|
||||
return output.buffer;
|
||||
@@ -501,43 +507,6 @@ release_get_latest(void)
|
||||
}
|
||||
}
|
||||
|
||||
// strict "major.minor.patch": the found version is untrusted network input
|
||||
static gboolean
|
||||
_parse_version(const char* const version, int* major, int* minor, int* patch)
|
||||
{
|
||||
if (!version) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int* out[3] = { major, minor, patch };
|
||||
const char* curr = version;
|
||||
|
||||
for (int part = 0; part < 3; part++) {
|
||||
if (part > 0) {
|
||||
if (*curr != '.') {
|
||||
return FALSE;
|
||||
}
|
||||
curr++;
|
||||
}
|
||||
|
||||
if (!g_ascii_isdigit(*curr)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gint64 value = 0;
|
||||
while (g_ascii_isdigit(*curr)) {
|
||||
value = value * 10 + (*curr - '0');
|
||||
if (value > G_MAXINT) {
|
||||
return FALSE;
|
||||
}
|
||||
curr++;
|
||||
}
|
||||
*out[part] = (int)value;
|
||||
}
|
||||
|
||||
return *curr == '\0';
|
||||
}
|
||||
|
||||
gboolean
|
||||
release_is_new(const char* const curr_version, const char* const found_version)
|
||||
{
|
||||
@@ -547,10 +516,12 @@ release_is_new(const char* const curr_version, const char* const found_version)
|
||||
|
||||
int curr_maj, curr_min, curr_patch, found_maj, found_min, found_patch;
|
||||
|
||||
gboolean parse_curr = _parse_version(curr_version, &curr_maj, &curr_min, &curr_patch);
|
||||
gboolean parse_found = _parse_version(found_version, &found_maj, &found_min, &found_patch);
|
||||
int parse_curr = sscanf(curr_version, "%d.%d.%d", &curr_maj, &curr_min,
|
||||
&curr_patch);
|
||||
int parse_found = sscanf(found_version, "%d.%d.%d", &found_maj, &found_min,
|
||||
&found_patch);
|
||||
|
||||
if (parse_found && parse_curr) {
|
||||
if (parse_found == 3 && parse_curr == 3) {
|
||||
if (found_maj > curr_maj) {
|
||||
return TRUE;
|
||||
} else if (found_maj == curr_maj && found_min > curr_min) {
|
||||
@@ -754,57 +725,22 @@ get_file_paths_recursive(const char* path, GSList** contents)
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_random_bytes(unsigned char* buf, size_t len)
|
||||
{
|
||||
#ifdef HAVE_LIBGCRYPT
|
||||
// never initialise gcrypt here: omemo_crypto_init() must set up secure memory first
|
||||
if (gcry_control(GCRYCTL_INITIALIZATION_FINISHED_P)) {
|
||||
gcry_create_nonce(buf, len);
|
||||
return TRUE;
|
||||
}
|
||||
#endif
|
||||
|
||||
FILE* urandom = fopen("/dev/urandom", "rb");
|
||||
if (urandom) {
|
||||
size_t got = fread(buf, 1, len, urandom);
|
||||
fclose(urandom);
|
||||
if (got == len) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
gchar*
|
||||
get_random_string(size_t length)
|
||||
{
|
||||
static const gchar alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const unsigned alphabet_len = sizeof(alphabet) - 1;
|
||||
const unsigned limit = 256 - (256 % alphabet_len); // reject the uneven tail, it would bias early letters
|
||||
GRand* prng;
|
||||
gchar* rand;
|
||||
gchar alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
int endrange = sizeof(alphabet) - 1;
|
||||
|
||||
gchar* rand = g_malloc0(length + 1);
|
||||
rand = g_malloc0(length + 1);
|
||||
|
||||
for (size_t i = 0; i < length;) {
|
||||
unsigned char block[64];
|
||||
size_t want = length - i;
|
||||
if (want > sizeof(block)) {
|
||||
want = sizeof(block);
|
||||
}
|
||||
prng = g_rand_new();
|
||||
|
||||
if (!_random_bytes(block, want)) {
|
||||
log_error("No cryptographic random source available for identifier generation");
|
||||
g_free(rand);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (size_t j = 0; j < want && i < length; j++) {
|
||||
if (block[j] < limit) {
|
||||
rand[i++] = alphabet[block[j] % alphabet_len];
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
rand[i] = alphabet[g_rand_int_range(prng, 0, endrange)];
|
||||
}
|
||||
g_rand_free(prng);
|
||||
|
||||
return rand;
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ gboolean strtoi_range(const char* str, int* saveptr, int min, int max, char** er
|
||||
gsize g_diff_to_gsize(const void* end, const void* start);
|
||||
int utf8_display_len(const char* const str);
|
||||
gchar* str_xml_sanitize(const char* const str);
|
||||
gchar* redact_secrets(const char* const str);
|
||||
|
||||
gboolean string_matches_one_of(const char* what, const char* is, gboolean is_can_be_null, const char* first, ...) __attribute__((sentinel));
|
||||
gboolean valid_tls_policy_option(const char* is);
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
#include <sys/statvfs.h>
|
||||
#include <sqlite3.h>
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -124,6 +125,15 @@ _get_db_filename(ProfAccount* account)
|
||||
return files_file_in_account_data_path(DIR_DATABASE, account->jid, "chatlog.db");
|
||||
}
|
||||
|
||||
static int
|
||||
_quick_check_cb(void* intact, int argc, char** argv, char** column_names)
|
||||
{
|
||||
if (argc > 0 && argv[0] && strcmp(argv[0], "ok") == 0) {
|
||||
*(gboolean*)intact = TRUE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_sqlite_init(ProfAccount* account)
|
||||
{
|
||||
@@ -149,6 +159,20 @@ _sqlite_init(ProfAccount* account)
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
g_chmod(filename, S_IRUSR | S_IWUSR); // history holds plaintext; journal/WAL files inherit these perms
|
||||
|
||||
// catch corruption before running queries or migrations against the file
|
||||
char* check_err = NULL;
|
||||
gboolean intact = FALSE;
|
||||
ret = sqlite3_exec(g_chatlog_database, "PRAGMA quick_check(1);", _quick_check_cb, &intact, &check_err);
|
||||
if (ret != SQLITE_OK || !intact) {
|
||||
log_error("Chat history database failed integrity check (%s): %s", filename,
|
||||
check_err ? check_err : "quick_check did not return 'ok'");
|
||||
sqlite3_free(check_err);
|
||||
_db_teardown("_sqlite_init(quick_check)");
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
char* err_msg = NULL;
|
||||
|
||||
int db_version = _get_db_version();
|
||||
@@ -691,7 +715,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
original_message_id = tmp ? tmp : original_message_id;
|
||||
|
||||
if (g_strcmp0(from_jid_orig, from_jid->barejid) != 0) {
|
||||
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s. Message: %s", from_jid->barejid, from_jid_orig, message->replace_id, message->plain);
|
||||
log_error("Mismatch in sender JIDs when trying to do LMC. Corrected message sender: %s. Original message sender: %s. Replace-ID: %s.", from_jid->barejid, from_jid_orig, message->replace_id);
|
||||
cons_show_error("%s sent a message correction with mismatched sender. See log for details.", from_jid->barejid);
|
||||
sqlite3_finalize(lmc_stmt);
|
||||
return;
|
||||
@@ -714,7 +738,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
sqlite3_stmt* stmt;
|
||||
if (_db_prepare_ctx(duplicate_check_query, &stmt, "_add_to_db(duplicate_check)")) {
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s; content: %s", message->id, message->stanzaid, from_jid->barejid, message->plain);
|
||||
log_error("Duplicate stanza-id found for the message. stanza_id: %s; archive_id: %s; sender: %s", message->id, message->stanzaid, from_jid->barejid);
|
||||
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -745,7 +769,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
return;
|
||||
}
|
||||
|
||||
log_debug("Writing to DB. Query: %s", query);
|
||||
log_debug("Writing message to DB (id: %s, stanza_id: %s, type: %s)", message->id, message->stanzaid, type); // no query text: it embeds the plaintext body
|
||||
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
if (err_msg) {
|
||||
@@ -757,7 +781,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
} else {
|
||||
int inserted_rows_count = sqlite3_changes(g_chatlog_database);
|
||||
if (inserted_rows_count < 1) {
|
||||
log_error("SQLite did not insert message (rows: %d, id: %s, content: %s)", inserted_rows_count, message->id, message->plain);
|
||||
log_error("SQLite did not insert message (rows: %d, id: %s)", inserted_rows_count, message->id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,11 +198,9 @@ cl_ev_send_muc_msg_corrected(ProfMucWin* mucwin, const char* const msg, const ch
|
||||
#ifdef HAVE_OMEMO
|
||||
if (mucwin->is_omemo) {
|
||||
auto_char char* id = omemo_on_message_send((ProfWin*)mucwin, message, FALSE, TRUE, replace_id);
|
||||
if (id != NULL) { // nothing was sent, so don't persist or echo the plaintext
|
||||
groupchat_log_omemo_msg_out(mucwin->roomjid, message);
|
||||
log_database_add_outgoing_muc(id, mucwin->roomjid, message, replace_id, PROF_MSG_ENC_OMEMO);
|
||||
mucwin_outgoing_msg(mucwin, message, id, PROF_MSG_ENC_OMEMO, replace_id);
|
||||
}
|
||||
groupchat_log_omemo_msg_out(mucwin->roomjid, message);
|
||||
log_database_add_outgoing_muc(id, mucwin->roomjid, message, replace_id, PROF_MSG_ENC_OMEMO);
|
||||
mucwin_outgoing_msg(mucwin, message, id, PROF_MSG_ENC_OMEMO, replace_id);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
|
||||
@@ -83,6 +83,7 @@ sv_ev_login_account_success(char* account_name, gboolean secured)
|
||||
|
||||
if (!log_database_init(account)) {
|
||||
log_error("Failed to initialize database for account: %s", account->jid);
|
||||
cons_show_error("Chat history storage is unavailable for this session, messages will not be saved. See the log for details.");
|
||||
}
|
||||
vcard_user_refresh();
|
||||
avatar_pep_subscribe();
|
||||
@@ -205,7 +206,7 @@ void
|
||||
sv_ev_failed_login(void)
|
||||
{
|
||||
cons_show_error("Login failed.");
|
||||
log_warning("[SECURITY] Authentication failed for %s", STR_MAYBE_NULL(session_get_account_name()));
|
||||
log_info("Login failed");
|
||||
tlscerts_clear_current();
|
||||
}
|
||||
|
||||
@@ -1145,11 +1146,6 @@ sv_ev_certfail(const char* const errormsg, const TLSCertificate* cert)
|
||||
return 1;
|
||||
}
|
||||
|
||||
log_warning("[SECURITY] TLS certificate verification failed: %s (subject: %s, fingerprint: %s)",
|
||||
errormsg,
|
||||
cert && cert->subjectname ? cert->subjectname : "(unknown)",
|
||||
cert && cert->fingerprint ? cert->fingerprint : "(none)");
|
||||
|
||||
cons_show("");
|
||||
cons_show_error("TLS certificate verification failed: %s", errormsg);
|
||||
cons_show_tlscert(cert);
|
||||
|
||||
@@ -286,7 +286,8 @@ log_stderr_handler(void)
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
if (buf[i] == '\n') {
|
||||
log_msg(stderr_level, "stderr", s->str);
|
||||
auto_gchar gchar* redacted = redact_secrets(s->str); // third-party libs may echo credentials
|
||||
log_msg(stderr_level, "stderr", redacted);
|
||||
g_string_assign(s, "");
|
||||
} else
|
||||
g_string_append_c(s, buf[i]);
|
||||
@@ -294,7 +295,8 @@ log_stderr_handler(void)
|
||||
} while (1);
|
||||
|
||||
if (s->len > 0 && s->str[0] != '\0') {
|
||||
log_msg(stderr_level, "stderr", s->str);
|
||||
auto_gchar gchar* redacted = redact_secrets(s->str);
|
||||
log_msg(stderr_level, "stderr", redacted);
|
||||
g_string_assign(s, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1433,12 +1433,6 @@ _omemo_fingerprint(ec_public_key* identity, gboolean formatted)
|
||||
size_t identity_public_key_len = signal_buffer_len(identity_public_key);
|
||||
unsigned char* identity_public_key_data = signal_buffer_data(identity_public_key);
|
||||
|
||||
if (identity_public_key_len == 0) {
|
||||
log_error("[OMEMO] cannot fingerprint an empty identity key"); // the decrement below would wrap
|
||||
signal_buffer_free(identity_public_key);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Skip first byte corresponding to signal DJB_TYPE */
|
||||
identity_public_key_len--;
|
||||
identity_public_key_data = &identity_public_key_data[1];
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <libotr/message.h>
|
||||
#include <libotr/sm.h>
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "chatlog.h"
|
||||
@@ -22,7 +24,6 @@
|
||||
#include "config/files.h"
|
||||
#include "otr/otr.h"
|
||||
#include "otr/otrlib.h"
|
||||
#include "event/client_events.h"
|
||||
#include "ui/ui.h"
|
||||
#include "ui/window_list.h"
|
||||
#include "xmpp/chat_session.h"
|
||||
@@ -115,6 +116,8 @@ cb_write_fingerprints(void* opdata)
|
||||
if (err != GPG_ERR_NO_ERROR) {
|
||||
log_error("Failed to write fingerprints file");
|
||||
cons_show_error("Failed to write fingerprints file");
|
||||
} else {
|
||||
g_chmod(fpsfilename, S_IRUSR | S_IWUSR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,10 +335,6 @@ otr_on_message_send(ProfChatWin* chatwin, const char* const message, gboolean re
|
||||
|
||||
// tag and send for policy opportunistic
|
||||
if (policy == PROF_OTRPOLICY_OPPORTUNISTIC) {
|
||||
// the tagged first message is still plaintext on the wire
|
||||
if (!allow_unencrypted_message(chatwin, message)) {
|
||||
return TRUE;
|
||||
}
|
||||
auto_char char* otr_tagged_msg = otr_tag_message(message);
|
||||
id = message_send_chat_otr(chatwin->barejid, otr_tagged_msg, request_receipt, replace_id);
|
||||
chatwin_outgoing_msg(chatwin, message, id, PROF_MSG_ENC_NONE, request_receipt, replace_id);
|
||||
@@ -381,6 +380,7 @@ otr_keygen(ProfAccount* account)
|
||||
cons_show_error("Failed to generate private key");
|
||||
return;
|
||||
}
|
||||
g_chmod(keysfilename->str, S_IRUSR | S_IWUSR);
|
||||
log_info("Private key generated");
|
||||
cons_show("");
|
||||
cons_show("Private key generation complete.");
|
||||
@@ -395,6 +395,7 @@ otr_keygen(ProfAccount* account)
|
||||
cons_show_error("Failed to create fingerprints file");
|
||||
return;
|
||||
}
|
||||
g_chmod(fpsfilename->str, S_IRUSR | S_IWUSR);
|
||||
log_info("Fingerprints file created");
|
||||
|
||||
err = otrl_privkey_read(user_state, keysfilename->str);
|
||||
|
||||
@@ -13,10 +13,12 @@
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <curl/curl.h>
|
||||
#include <gio/gio.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <pthread.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
@@ -62,14 +64,28 @@ aesgcm_file_get(void* userdata)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Open the target file for storing the cleartext.
|
||||
auto_FILE FILE* outfh = fopen(aesgcm_dl->filename, "wb");
|
||||
// Decrypt into a temporary file next to the target and rename it into
|
||||
// place only after the GCM tag verified, so tampered or truncated
|
||||
// content never appears at the destination path.
|
||||
auto_gchar gchar* partname = g_strdup_printf("%s.part.XXXXXX", aesgcm_dl->filename);
|
||||
gint outfd = g_mkstemp(partname);
|
||||
if (outfd == -1) {
|
||||
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR,
|
||||
"Downloading '%s' failed: Unable to open "
|
||||
"output file at '%s' for writing (%s).",
|
||||
https_url, aesgcm_dl->filename,
|
||||
g_strerror(errno));
|
||||
return NULL;
|
||||
}
|
||||
FILE* outfh = fdopen(outfd, "wb");
|
||||
if (outfh == NULL) {
|
||||
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR,
|
||||
"Downloading '%s' failed: Unable to open "
|
||||
"output file at '%s' for writing (%s).",
|
||||
https_url, aesgcm_dl->filename,
|
||||
g_strerror(errno));
|
||||
close(outfd);
|
||||
remove(partname);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -90,6 +106,8 @@ aesgcm_file_get(void* userdata)
|
||||
|
||||
ssize_t* p_bytes_received = http_file_get(http_dl);
|
||||
if (!p_bytes_received) {
|
||||
fclose(outfh);
|
||||
remove(partname);
|
||||
return NULL;
|
||||
}
|
||||
ssize_t bytes_received = *p_bytes_received;
|
||||
@@ -102,6 +120,8 @@ aesgcm_file_get(void* userdata)
|
||||
"temporary file at '%s' for reading (%s).",
|
||||
aesgcm_dl->url, tmpname,
|
||||
g_strerror(errno));
|
||||
fclose(outfh);
|
||||
remove(partname);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -110,20 +130,32 @@ aesgcm_file_get(void* userdata)
|
||||
bytes_received, fragment);
|
||||
fclose(tmpfh);
|
||||
remove(tmpname);
|
||||
fclose(outfh);
|
||||
|
||||
gboolean saved = FALSE;
|
||||
if (crypt_res != GPG_ERR_NO_ERROR) {
|
||||
remove(partname);
|
||||
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR,
|
||||
"Downloading '%s' failed: Failed to decrypt "
|
||||
"file (%s).",
|
||||
https_url, gcry_strerror(crypt_res));
|
||||
} else if (g_rename(partname, aesgcm_dl->filename) != 0) {
|
||||
remove(partname);
|
||||
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ERROR, ENTRY_ERROR,
|
||||
"Downloading '%s' failed: Unable to move "
|
||||
"decrypted file to '%s' (%s).",
|
||||
https_url, aesgcm_dl->filename,
|
||||
g_strerror(errno));
|
||||
} else {
|
||||
saved = TRUE;
|
||||
http_print_transfer_update(aesgcm_dl->window, aesgcm_dl->id, THEME_ONLINE, ENTRY_COMPLETED,
|
||||
"Downloading '%s': done\nSaved to '%s'",
|
||||
aesgcm_dl->url, aesgcm_dl->filename);
|
||||
win_mark_received(aesgcm_dl->window, aesgcm_dl->id);
|
||||
}
|
||||
|
||||
if (aesgcm_dl->cmd_template != NULL) {
|
||||
// never hand an unverified file to the external command
|
||||
if (saved && aesgcm_dl->cmd_template != NULL) {
|
||||
gchar** argv = format_call_external_argv(aesgcm_dl->cmd_template,
|
||||
aesgcm_dl->filename,
|
||||
aesgcm_dl->filename);
|
||||
@@ -140,8 +172,8 @@ aesgcm_file_get(void* userdata)
|
||||
}
|
||||
|
||||
g_strfreev(argv);
|
||||
free(aesgcm_dl->cmd_template);
|
||||
}
|
||||
free(aesgcm_dl->cmd_template);
|
||||
|
||||
free(aesgcm_dl->id);
|
||||
free(aesgcm_dl->filename);
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#include <string.h>
|
||||
#include <gio/gio.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "tools/http_common.h"
|
||||
|
||||
#define FALLBACK_MSG ""
|
||||
@@ -52,13 +51,3 @@ http_print_transfer(ProfWin* window, char* id, theme_item_t theme_item, const ch
|
||||
|
||||
g_string_free(msg, TRUE);
|
||||
}
|
||||
|
||||
void
|
||||
http_warn_insecure_transfer(ProfWin* window, char* id, const char* const url)
|
||||
{
|
||||
log_warning("[SECURITY] TLS certificate verification disabled for transfer: %s", url);
|
||||
http_print_transfer_update(window, id, THEME_ERROR, 0,
|
||||
"Security warning: TLS certificate not verified for '%s' "
|
||||
"(tls.policy=trust).",
|
||||
url);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,5 @@ G_GNUC_PRINTF(4, 5)
|
||||
void http_print_transfer(ProfWin* window, char* id, theme_item_t theme_item, const char* fmt, ...);
|
||||
G_GNUC_PRINTF(5, 6)
|
||||
void http_print_transfer_update(ProfWin* window, char* id, theme_item_t theme_item, int flags, const char* fmt, ...);
|
||||
void http_warn_insecure_transfer(ProfWin* window, char* id, const char* const url);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
#include "profanity.h"
|
||||
#include "event/client_events.h"
|
||||
#include "tools/http_download.h"
|
||||
#include "tools/http_common.h"
|
||||
#include "config/cafile.h"
|
||||
#include "config/preferences.h"
|
||||
#include "ui/ui.h"
|
||||
@@ -154,8 +153,6 @@ http_file_get(void* userdata)
|
||||
if (insecure) {
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
http_warn_insecure_transfer(download->window, download->id,
|
||||
download->display_url ?: download->url);
|
||||
}
|
||||
|
||||
if ((res = curl_easy_perform(curl)) != CURLE_OK) {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
#include "profanity.h"
|
||||
#include "event/client_events.h"
|
||||
#include "tools/http_upload.h"
|
||||
#include "tools/http_common.h"
|
||||
#include "config/cafile.h"
|
||||
#include "config/preferences.h"
|
||||
#include "ui/ui.h"
|
||||
@@ -243,7 +242,6 @@ http_file_put(void* userdata)
|
||||
if (insecure) {
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
http_warn_insecure_transfer(upload->window, upload->put_url, upload->put_url);
|
||||
}
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_READDATA, fh);
|
||||
|
||||
@@ -46,7 +46,6 @@ typedef struct prof_conn_t
|
||||
GHashTable* available_resources;
|
||||
GHashTable* features_by_jid;
|
||||
GHashTable* requested_features;
|
||||
gboolean tls_disabled_by_user;
|
||||
} ProfConnection;
|
||||
|
||||
typedef struct
|
||||
@@ -213,21 +212,6 @@ _conn_apply_settings(const char* const jid, const char* const passwd, const char
|
||||
#undef LOG_FLAG_IF_SET
|
||||
}
|
||||
|
||||
conn.tls_disabled_by_user = (flags & XMPP_CONN_FLAG_DISABLE_TLS) ? TRUE : FALSE;
|
||||
|
||||
if (flags & XMPP_CONN_FLAG_DISABLE_TLS) {
|
||||
log_warning("[SECURITY] TLS is disabled for this connection: traffic and credentials are sent in the clear");
|
||||
cons_show_error("Security warning: TLS is disabled, this connection is unencrypted.");
|
||||
}
|
||||
if (flags & XMPP_CONN_FLAG_TRUST_TLS) {
|
||||
log_warning("[SECURITY] TLS certificate verification is disabled for this connection");
|
||||
cons_show_error("Security warning: TLS certificates are not verified for this connection.");
|
||||
}
|
||||
if (flags & XMPP_CONN_FLAG_LEGACY_AUTH) {
|
||||
log_warning("[SECURITY] Legacy (XEP-0078) authentication enabled: the password is sent without SASL");
|
||||
cons_show_error("Security warning: legacy authentication is enabled for this connection.");
|
||||
}
|
||||
|
||||
if (xmpp_conn_set_flags(conn.xmpp_conn, flags)) {
|
||||
log_error("libstrophe doesn't accept this combination of flags: 0x%lx", flags);
|
||||
conn.conn_status = JABBER_DISCONNECTED;
|
||||
@@ -380,8 +364,7 @@ _register_handle_proceedtls_default(xmpp_conn_t* xmpp_conn,
|
||||
xmpp_handler_delete(xmpp_conn, _register_handle_error);
|
||||
xmpp_conn_open_stream_default(xmpp_conn);
|
||||
} else {
|
||||
log_warning("[SECURITY] TLS handshake failed during registration, aborting");
|
||||
cons_show_error("Security warning: TLS handshake failed, registration aborted.");
|
||||
log_debug("TLS failed.");
|
||||
/* failed tls spoils the connection, so disconnect */
|
||||
xmpp_disconnect(xmpp_conn);
|
||||
}
|
||||
@@ -425,14 +408,6 @@ _register_handle_features(xmpp_conn_t* xmpp_conn, xmpp_stanza_t* stanza, void* u
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Registration sends the new password in this stream: refuse to do so in the clear */
|
||||
if (!xmpp_conn_is_secured(xmpp_conn)) {
|
||||
log_warning("[SECURITY] Refusing in-band registration over an unencrypted stream");
|
||||
cons_show_error("Registration aborted: the connection is not encrypted and the password would be sent in the clear.");
|
||||
xmpp_disconnect(xmpp_conn);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* check whether server supports in-band registration */
|
||||
child = xmpp_stanza_get_child_by_name(stanza, "register");
|
||||
if (!child) {
|
||||
@@ -982,12 +957,6 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
|
||||
conn.features_by_jid = g_hash_table_new_full(g_str_hash, g_str_equal, free, (GDestroyNotify)g_hash_table_destroy);
|
||||
g_hash_table_insert(conn.features_by_jid, strdup(conn.domain), g_hash_table_new_full(g_str_hash, g_str_equal, free, NULL));
|
||||
|
||||
// tls.policy=allow negotiates opportunistically, so an unrequested downgrade must not pass silently
|
||||
if (!connection_is_secured() && !conn.tls_disabled_by_user) {
|
||||
log_warning("[SECURITY] Logged in over an unencrypted connection to %s: the server offered no usable TLS", conn.domain);
|
||||
cons_show_error("Security warning: this session is NOT encrypted, the server did not provide TLS. Use '/account set <account> tls force' to require it.");
|
||||
}
|
||||
|
||||
session_login_success(connection_is_secured());
|
||||
|
||||
if (conn.queued_messages) {
|
||||
@@ -1054,9 +1023,8 @@ _connection_handler(xmpp_conn_t* const xmpp_conn, const xmpp_conn_event_t status
|
||||
int port;
|
||||
if (stream_error && stream_error->stanza && _get_other_host(stream_error->stanza, &host, &port)) {
|
||||
g_assert(port >= 0 && port <= UINT16_MAX);
|
||||
log_warning("[SECURITY] Server redirected the connection (see-other-host) to \"%s\":%d", host, port);
|
||||
cons_show_error("Security notice: the server redirected this connection to %s:%d.", host, port);
|
||||
session_reconnect(host, (unsigned short)port);
|
||||
log_debug("Connection handler: Forcing a re-connect to \"%s\"", host);
|
||||
conn.conn_status = JABBER_RECONNECT;
|
||||
return;
|
||||
}
|
||||
@@ -1136,7 +1104,8 @@ _xmpp_file_logger(void* const userdata, const xmpp_log_level_t xmpp_level, const
|
||||
break;
|
||||
}
|
||||
|
||||
log_msg(prof_level, area, msg);
|
||||
auto_gchar gchar* redacted = redact_secrets(msg); // raw traffic contains SASL/register credentials
|
||||
log_msg(prof_level, area, redacted);
|
||||
|
||||
if ((g_strcmp0(area, "xmpp") == 0) || (g_strcmp0(area, "conn")) == 0) {
|
||||
sv_ev_xmpp_stanza(msg);
|
||||
@@ -1153,12 +1122,8 @@ _random_bytes_init(void)
|
||||
profanity_instance_id = g_key_file_get_string(keyfile.keyfile, "identifier", "random_bytes", NULL);
|
||||
} else {
|
||||
profanity_instance_id = get_random_string(10);
|
||||
if (profanity_instance_id) {
|
||||
g_key_file_set_string(keyfile.keyfile, "identifier", "random_bytes", profanity_instance_id);
|
||||
save_keyfile(&keyfile);
|
||||
} else {
|
||||
log_error("Could not generate an instance identifier: no random source available");
|
||||
}
|
||||
g_key_file_set_string(keyfile.keyfile, "identifier", "random_bytes", profanity_instance_id);
|
||||
save_keyfile(&keyfile);
|
||||
}
|
||||
|
||||
free_keyfile(&keyfile);
|
||||
|
||||
@@ -117,7 +117,6 @@ main(int argc, char* argv[])
|
||||
PROF_FUNC_TEST(connect_jid_requests_bookmarks),
|
||||
PROF_FUNC_TEST(connect_bad_password),
|
||||
PROF_FUNC_TEST(connect_shows_presence_updates),
|
||||
PROF_FUNC_TEST(connect_warns_on_insecure_transport),
|
||||
|
||||
/* Ping tests - XEP-0199 XMPP Ping */
|
||||
PROF_FUNC_TEST(ping_server),
|
||||
@@ -256,6 +255,9 @@ main(int argc, char* argv[])
|
||||
PROF_FUNC_TEST(message_db_history_verify),
|
||||
PROF_FUNC_TEST(message_db_history_lmc),
|
||||
PROF_FUNC_TEST(message_db_history_multi_resource),
|
||||
#ifdef HAVE_SQLITE
|
||||
PROF_FUNC_TEST(message_db_corrupt_database_degrades_gracefully),
|
||||
#endif
|
||||
|
||||
/* Basic message send/receive */
|
||||
PROF_FUNC_TEST(message_send),
|
||||
|
||||
@@ -92,15 +92,3 @@ connect_shows_presence_updates(void **state)
|
||||
);
|
||||
assert_true(prof_output_exact("Buddy1 (mobile) is xa, \"Gone :(\""));
|
||||
}
|
||||
|
||||
void
|
||||
connect_warns_on_insecure_transport(void **state)
|
||||
{
|
||||
/* the harness connects with "tls disable auth legacy": both downgrades must reach the console */
|
||||
prof_connect();
|
||||
|
||||
prof_timeout(10);
|
||||
assert_true(prof_output_exact("Security warning: TLS is disabled, this connection is unencrypted."));
|
||||
assert_true(prof_output_exact("Security warning: legacy authentication is enabled for this connection."));
|
||||
prof_timeout_reset();
|
||||
}
|
||||
|
||||
@@ -3,4 +3,3 @@ void connect_jid_sends_presence_after_receiving_roster(void **state);
|
||||
void connect_jid_requests_bookmarks(void **state);
|
||||
void connect_bad_password(void **state);
|
||||
void connect_shows_presence_updates(void **state);
|
||||
void connect_warns_on_insecure_transport(void **state);
|
||||
|
||||
@@ -536,3 +536,41 @@ message_db_history_multi_resource(void** state)
|
||||
assert_true(prof_output_regex("Buddy1/laptop"));
|
||||
assert_true(prof_output_regex("Buddy1/tablet"));
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: corrupt chatlog.db degrades gracefully (issue #146, REQ-RES-02).
|
||||
*
|
||||
* A chatlog.db with a valid SQLite magic but garbage content is planted
|
||||
* before connecting. Database init must fail cleanly: the user gets a
|
||||
* console warning, the session stays up, and the client stays responsive.
|
||||
*/
|
||||
void
|
||||
message_db_corrupt_database_degrades_gracefully(void** state)
|
||||
{
|
||||
const char* xdg_data = getenv("XDG_DATA_HOME");
|
||||
assert_non_null(xdg_data);
|
||||
|
||||
GString* db_file = g_string_new(xdg_data);
|
||||
g_string_append(db_file, "/profanity/database/stabber_at_localhost");
|
||||
assert_int_equal(0, g_mkdir_with_parents(db_file->str, 0700));
|
||||
g_string_append(db_file, "/chatlog.db");
|
||||
|
||||
/* valid 16-byte SQLite header magic followed by garbage: sqlite3_open
|
||||
* succeeds (lazy open), the integrity gate must catch it */
|
||||
FILE* db = fopen(db_file->str, "wb");
|
||||
assert_non_null(db);
|
||||
assert_int_equal(16, fwrite("SQLite format 3", 1, 16, db));
|
||||
for (int i = 0; i < 4096; i++) {
|
||||
fputc(0xA5, db);
|
||||
}
|
||||
fclose(db);
|
||||
g_string_free(db_file, TRUE);
|
||||
|
||||
prof_connect();
|
||||
|
||||
assert_true(prof_output_exact("Chat history storage is unavailable for this session"));
|
||||
|
||||
/* client is still alive and responsive after the failed DB init */
|
||||
prof_input("/autoping set 60");
|
||||
assert_true(prof_output_exact("Autoping interval set to 60 seconds."));
|
||||
}
|
||||
|
||||
@@ -11,3 +11,4 @@ void message_db_history_service_chars(void** state);
|
||||
void message_db_history_verify(void** state);
|
||||
void message_db_history_lmc(void** state);
|
||||
void message_db_history_multi_resource(void** state);
|
||||
void message_db_corrupt_database_degrades_gracefully(void** state);
|
||||
|
||||
152
tests/unittests/omemo/test_omemo_crypto.c
Normal file
152
tests/unittests/omemo/test_omemo_crypto.c
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* test_omemo_crypto.c
|
||||
*
|
||||
* Unit tests for the OMEMO AES-256-GCM file crypto (src/omemo/crypto.c).
|
||||
* The decrypt direction streams plaintext before the tag is checked, so
|
||||
* callers rely on the returned error code to discard unverified output —
|
||||
* these tests pin that contract (issue #146, REQ-CRY-06).
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <glib.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include "prof_cmocka.h"
|
||||
|
||||
#ifdef HAVE_OMEMO
|
||||
|
||||
#include "omemo/omemo.h"
|
||||
#include "omemo/crypto.h"
|
||||
|
||||
#define TAG_LENGTH 16
|
||||
|
||||
static const unsigned char PLAINTEXT[] = "at-rest integrity check payload: 0123456789abcdef";
|
||||
|
||||
// gcrypt secure memory must be set up exactly once per process
|
||||
static int
|
||||
_crypto_init_once(void)
|
||||
{
|
||||
static gboolean done = FALSE;
|
||||
static int rc = 0;
|
||||
if (!done) {
|
||||
rc = omemo_crypto_init();
|
||||
done = TRUE;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
static off_t
|
||||
_file_size(FILE* fh)
|
||||
{
|
||||
fseeko(fh, 0, SEEK_END);
|
||||
off_t size = ftello(fh);
|
||||
rewind(fh);
|
||||
return size;
|
||||
}
|
||||
|
||||
// encrypt PLAINTEXT with a fixed key/nonce into a fresh tmpfile
|
||||
static FILE*
|
||||
_encrypted_tmpfile(unsigned char* key, unsigned char* nonce)
|
||||
{
|
||||
memset(key, 0x42, OMEMO_AESGCM_KEY_LENGTH);
|
||||
memset(nonce, 0x24, OMEMO_AESGCM_NONCE_LENGTH);
|
||||
|
||||
FILE* plain = tmpfile();
|
||||
FILE* cipher = tmpfile();
|
||||
assert_non_null(plain);
|
||||
assert_non_null(cipher);
|
||||
|
||||
assert_int_equal(sizeof(PLAINTEXT), fwrite(PLAINTEXT, 1, sizeof(PLAINTEXT), plain));
|
||||
rewind(plain);
|
||||
|
||||
assert_int_equal(GPG_ERR_NO_ERROR,
|
||||
aes256gcm_crypt_file(plain, cipher, (off_t)sizeof(PLAINTEXT), key, nonce, TRUE));
|
||||
fclose(plain);
|
||||
rewind(cipher);
|
||||
return cipher;
|
||||
}
|
||||
|
||||
// corrupt one byte at offset (negative counts from the end), return reopened stream
|
||||
static FILE*
|
||||
_flip_byte(FILE* cipher, long offset)
|
||||
{
|
||||
off_t size = _file_size(cipher);
|
||||
unsigned char* buf = g_malloc(size);
|
||||
assert_int_equal(size, fread(buf, 1, size, cipher));
|
||||
fclose(cipher);
|
||||
|
||||
long pos = offset >= 0 ? offset : (long)size + offset;
|
||||
buf[pos] ^= 0xFF;
|
||||
|
||||
FILE* tampered = tmpfile();
|
||||
assert_non_null(tampered);
|
||||
assert_int_equal(size, fwrite(buf, 1, size, tampered));
|
||||
rewind(tampered);
|
||||
g_free(buf);
|
||||
return tampered;
|
||||
}
|
||||
|
||||
void
|
||||
aes256gcm_crypt_file__roundtrip_succeeds(void** state)
|
||||
{
|
||||
assert_int_equal(0, _crypto_init_once());
|
||||
|
||||
unsigned char key[OMEMO_AESGCM_KEY_LENGTH];
|
||||
unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
|
||||
FILE* cipher = _encrypted_tmpfile(key, nonce);
|
||||
|
||||
off_t cipher_size = _file_size(cipher);
|
||||
assert_int_equal((off_t)sizeof(PLAINTEXT) + TAG_LENGTH, cipher_size);
|
||||
|
||||
FILE* decrypted = tmpfile();
|
||||
assert_non_null(decrypted);
|
||||
assert_int_equal(GPG_ERR_NO_ERROR,
|
||||
aes256gcm_crypt_file(cipher, decrypted, cipher_size, key, nonce, FALSE));
|
||||
|
||||
unsigned char readback[sizeof(PLAINTEXT)];
|
||||
rewind(decrypted);
|
||||
assert_int_equal(sizeof(PLAINTEXT), fread(readback, 1, sizeof(readback), decrypted));
|
||||
assert_memory_equal(PLAINTEXT, readback, sizeof(PLAINTEXT));
|
||||
|
||||
fclose(cipher);
|
||||
fclose(decrypted);
|
||||
}
|
||||
|
||||
void
|
||||
aes256gcm_crypt_file__rejects_tampered_tag(void** state)
|
||||
{
|
||||
assert_int_equal(0, _crypto_init_once());
|
||||
|
||||
unsigned char key[OMEMO_AESGCM_KEY_LENGTH];
|
||||
unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
|
||||
FILE* cipher = _flip_byte(_encrypted_tmpfile(key, nonce), -1); // last tag byte
|
||||
|
||||
FILE* decrypted = tmpfile();
|
||||
assert_non_null(decrypted);
|
||||
gcry_error_t res = aes256gcm_crypt_file(cipher, decrypted, _file_size(cipher), key, nonce, FALSE);
|
||||
assert_int_not_equal(GPG_ERR_NO_ERROR, res);
|
||||
|
||||
fclose(cipher);
|
||||
fclose(decrypted);
|
||||
}
|
||||
|
||||
void
|
||||
aes256gcm_crypt_file__rejects_tampered_ciphertext(void** state)
|
||||
{
|
||||
assert_int_equal(0, _crypto_init_once());
|
||||
|
||||
unsigned char key[OMEMO_AESGCM_KEY_LENGTH];
|
||||
unsigned char nonce[OMEMO_AESGCM_NONCE_LENGTH];
|
||||
FILE* cipher = _flip_byte(_encrypted_tmpfile(key, nonce), 0); // first payload byte
|
||||
|
||||
FILE* decrypted = tmpfile();
|
||||
assert_non_null(decrypted);
|
||||
gcry_error_t res = aes256gcm_crypt_file(cipher, decrypted, _file_size(cipher), key, nonce, FALSE);
|
||||
assert_int_not_equal(GPG_ERR_NO_ERROR, res);
|
||||
|
||||
fclose(cipher);
|
||||
fclose(decrypted);
|
||||
}
|
||||
|
||||
#endif
|
||||
8
tests/unittests/omemo/test_omemo_crypto.h
Normal file
8
tests/unittests/omemo/test_omemo_crypto.h
Normal file
@@ -0,0 +1,8 @@
|
||||
/* test_omemo_crypto.h
|
||||
*
|
||||
* Unit tests for OMEMO AES-256-GCM file crypto (issue #146, REQ-CRY-06)
|
||||
*/
|
||||
|
||||
void aes256gcm_crypt_file__roundtrip_succeeds(void** state);
|
||||
void aes256gcm_crypt_file__rejects_tampered_tag(void** state);
|
||||
void aes256gcm_crypt_file__rejects_tampered_ciphertext(void** state);
|
||||
@@ -1089,45 +1089,6 @@ release_is_new__tests__various(void** state)
|
||||
assert_false(release_is_new("0.16.0", ""));
|
||||
assert_false(release_is_new(NULL, "1.0.0"));
|
||||
assert_false(release_is_new("1.0.0", NULL));
|
||||
|
||||
// the found version is untrusted network input: nothing but strictly N.N.N may parse
|
||||
assert_false(release_is_new("0.16.0", " 1.0.0")); // leading whitespace
|
||||
assert_false(release_is_new("0.16.0", "+1.0.0")); // explicit sign
|
||||
assert_false(release_is_new("0.16.0", "-1.0.0")); // negative major
|
||||
assert_false(release_is_new("0.16.0", "1.0.0-rc1")); // trailing garbage
|
||||
assert_false(release_is_new("0.16.0", "1.0.0\n")); // trailing newline
|
||||
assert_false(release_is_new("0.16.0", "1..0")); // empty component
|
||||
assert_false(release_is_new("0.16.0", "0x10.0.0")); // hex is not accepted
|
||||
assert_false(release_is_new("0.16.0", "99999999999.0.0")); // out of int range, must not wrap
|
||||
assert_false(release_is_new("0.16.0", "1.0.0 malicious"));
|
||||
|
||||
// Boundary: the largest still-parsable component is accepted
|
||||
assert_true(release_is_new("0.16.0", "2147483647.0.0"));
|
||||
}
|
||||
|
||||
void
|
||||
get_random_string__generates_valid_ids(void** state)
|
||||
{
|
||||
const gchar* alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
|
||||
for (size_t len = 1; len <= 40; len++) {
|
||||
gchar* id = get_random_string(len);
|
||||
assert_non_null(id);
|
||||
assert_int_equal(len, strlen(id));
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
assert_non_null(strchr(alphabet, id[i]));
|
||||
}
|
||||
g_free(id);
|
||||
}
|
||||
|
||||
// a collision here would mean a dead random source
|
||||
gchar* a = get_random_string(20);
|
||||
gchar* b = get_random_string(20);
|
||||
assert_non_null(a);
|
||||
assert_non_null(b);
|
||||
assert_string_not_equal(a, b);
|
||||
g_free(a);
|
||||
g_free(b);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1423,3 +1384,55 @@ str_xml_sanitize__strips_illegal_characters(void** state)
|
||||
assert_string_equal("UTF-8: üñîçøðé and more", res5);
|
||||
g_free(res5);
|
||||
}
|
||||
|
||||
void
|
||||
redact_secrets__masks_credentials(void** state)
|
||||
{
|
||||
// NULL input
|
||||
assert_null(redact_secrets(NULL));
|
||||
|
||||
// Plain text and non-secret XML pass through unchanged
|
||||
gchar* res1 = redact_secrets("hello world");
|
||||
assert_string_equal("hello world", res1);
|
||||
g_free(res1);
|
||||
|
||||
gchar* res2 = redact_secrets("<message><body>secret-looking text</body></message>");
|
||||
assert_string_equal("<message><body>secret-looking text</body></message>", res2);
|
||||
g_free(res2);
|
||||
|
||||
// SASL auth payload is redacted, envelope kept
|
||||
gchar* res3 = redact_secrets("SENT: <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>AGFsaWNlAHBhc3N3b3Jk</auth>");
|
||||
assert_string_equal("SENT: <auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>[REDACTED]</auth>", res3);
|
||||
g_free(res3);
|
||||
|
||||
// SASL challenge/response round-trip
|
||||
gchar* res4 = redact_secrets("<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cj1abc</challenge>");
|
||||
assert_string_equal("<challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>[REDACTED]</challenge>", res4);
|
||||
g_free(res4);
|
||||
|
||||
gchar* res5 = redact_secrets("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>Yz1iaXdz</response>");
|
||||
assert_string_equal("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>[REDACTED]</response>", res5);
|
||||
g_free(res5);
|
||||
|
||||
// Empty SASL response element has no content to redact
|
||||
gchar* res6 = redact_secrets("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>");
|
||||
assert_string_equal("<response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>", res6);
|
||||
g_free(res6);
|
||||
|
||||
// XEP-0077 registration: password redacted, username kept
|
||||
gchar* res7 = redact_secrets("<query xmlns='jabber:iq:register'><username>alice</username><password>hunter2</password></query>");
|
||||
assert_string_equal("<query xmlns='jabber:iq:register'><username>alice</username><password>[REDACTED]</password></query>", res7);
|
||||
g_free(res7);
|
||||
|
||||
// XEP-0078 legacy auth: password-derived digest redacted
|
||||
gchar* res8 = redact_secrets("<query xmlns='jabber:iq:auth'><username>alice</username><digest>48fc78be9ec8f86d8ce1c39ebd7a5b4c9d0e2f13</digest><resource>tui</resource></query>");
|
||||
assert_string_equal("<query xmlns='jabber:iq:auth'><username>alice</username><digest>[REDACTED]</digest><resource>tui</resource></query>", res8);
|
||||
g_free(res8);
|
||||
|
||||
// invalid UTF-8 must not make redaction fail open
|
||||
gchar* res9 = redact_secrets("\xFF garbage <password>hunter2</password>");
|
||||
assert_non_null(res9);
|
||||
assert_null(strstr(res9, "hunter2"));
|
||||
assert_non_null(strstr(res9, "[REDACTED]"));
|
||||
g_free(res9);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,6 @@ void valid_tls_policy_option__is__correct_for_various_inputs(void** state);
|
||||
void get_mentions__tests__various(void** state);
|
||||
void release_is_new__tests__various(void** state);
|
||||
void str_xml_sanitize__strips_illegal_characters(void** state);
|
||||
void get_random_string__generates_valid_ids(void** state);
|
||||
void redact_secrets__masks_credentials(void** state);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
#include "test_ai_client.h"
|
||||
#include "test_database_export.h"
|
||||
#include "test_database_stress.h"
|
||||
#include "omemo/test_omemo_crypto.h"
|
||||
|
||||
#define muc_unit_test(f) cmocka_unit_test_setup_teardown(f, muc_before_test, muc_after_test)
|
||||
|
||||
@@ -687,7 +688,12 @@ main(int argc, char* argv[])
|
||||
cmocka_unit_test(get_mentions__tests__various),
|
||||
cmocka_unit_test(release_is_new__tests__various),
|
||||
cmocka_unit_test(str_xml_sanitize__strips_illegal_characters),
|
||||
cmocka_unit_test(get_random_string__generates_valid_ids),
|
||||
cmocka_unit_test(redact_secrets__masks_credentials),
|
||||
#ifdef HAVE_OMEMO
|
||||
cmocka_unit_test(aes256gcm_crypt_file__roundtrip_succeeds),
|
||||
cmocka_unit_test(aes256gcm_crypt_file__rejects_tampered_tag),
|
||||
cmocka_unit_test(aes256gcm_crypt_file__rejects_tampered_ciphertext),
|
||||
#endif
|
||||
|
||||
cmocka_unit_test_setup_teardown(plugins_get_command_names__returns__no_commands,
|
||||
load_preferences,
|
||||
|
||||
Reference in New Issue
Block a user