Compare commits
2 Commits
fix/data-a
...
feat/disco
| Author | SHA1 | Date | |
|---|---|---|---|
|
b7dec705c3
|
|||
|
b208e48ee1
|
44
.github/workflows/docker-publish.yml
vendored
44
.github/workflows/docker-publish.yml
vendored
@@ -1,44 +0,0 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051
|
||||
with:
|
||||
images: cproofdev/cproof
|
||||
tags: |
|
||||
type=raw,value=dev,enable={{is_default_branch}}
|
||||
type=sha,prefix=nightly-,enable={{is_default_branch}}
|
||||
type=ref,event=tag
|
||||
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@2bd26e71295ee32cbf6a73510d165bf7232460f3
|
||||
with:
|
||||
context: .
|
||||
file: ./ci/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
@@ -236,10 +236,7 @@ omemo_sources = \
|
||||
src/tools/aesgcm_download.h src/tools/aesgcm_download.c
|
||||
|
||||
omemo_unittest_sources = \
|
||||
tests/unittests/omemo/stub_omemo.c \
|
||||
tests/unittests/omemo/test_omemo_crypto.c \
|
||||
tests/unittests/omemo/test_omemo_crypto.h \
|
||||
src/omemo/crypto.c
|
||||
tests/unittests/omemo/stub_omemo.c
|
||||
|
||||
if BUILD_PYTHON_API
|
||||
core_sources += $(python_sources)
|
||||
|
||||
106
ci/Dockerfile
106
ci/Dockerfile
@@ -1,106 +0,0 @@
|
||||
# =============================================================================
|
||||
# Stage 1: Build
|
||||
# =============================================================================
|
||||
FROM ubuntu:26.04 AS builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
build-essential \
|
||||
autoconf \
|
||||
automake \
|
||||
autoconf-archive \
|
||||
ca-certificates \
|
||||
libtool \
|
||||
pkg-config \
|
||||
git \
|
||||
wget \
|
||||
libglib2.0-dev \
|
||||
libcurl4-openssl-dev \
|
||||
libsqlite3-dev \
|
||||
libncurses-dev \
|
||||
libreadline-dev \
|
||||
libnotify-dev \
|
||||
python3-dev \
|
||||
python3 \
|
||||
libgpgme-dev \
|
||||
libotr5-dev \
|
||||
libgtk-3-dev \
|
||||
libgdk-pixbuf-2.0-dev \
|
||||
libsignal-protocol-c-dev \
|
||||
libgcrypt20-dev \
|
||||
libqrencode-dev \
|
||||
libxss-dev \
|
||||
libcmocka-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Build libstrophe from source (not available as Ubuntu package)
|
||||
RUN git clone --depth 1 https://git.jabber.space/devs/libstrophe-gh-mirror.git /tmp/libstrophe \
|
||||
&& cd /tmp/libstrophe \
|
||||
&& autoreconf -i \
|
||||
&& ./configure --prefix=/usr \
|
||||
&& make -j$(nproc) \
|
||||
&& make install \
|
||||
&& rm -rf /tmp/libstrophe
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN ./bootstrap.sh \
|
||||
&& ./configure \
|
||||
--enable-notifications \
|
||||
--enable-icons-and-clipboard \
|
||||
--enable-otr \
|
||||
--enable-pgp \
|
||||
--enable-omemo \
|
||||
--enable-plugins \
|
||||
--enable-c-plugins \
|
||||
--enable-python-plugins \
|
||||
--with-xscreensaver \
|
||||
--enable-omemo-qrcode \
|
||||
--enable-gdk-pixbuf \
|
||||
&& make -j$(nproc) \
|
||||
&& make install
|
||||
|
||||
# =============================================================================
|
||||
# Stage 2: Runtime (minimal)
|
||||
# =============================================================================
|
||||
FROM ubuntu:26.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
libglib2.0-0t64 \
|
||||
libcurl4t64 \
|
||||
libsqlite3-0 \
|
||||
libncursesw6 \
|
||||
libreadline8t64 \
|
||||
libnotify4 \
|
||||
python3 \
|
||||
libpython3.14 \
|
||||
libgpgme45 \
|
||||
libotr5t64 \
|
||||
libgtk-3-0t64 \
|
||||
libgdk-pixbuf-2.0-0 \
|
||||
libsignal-protocol-c2.3.2 \
|
||||
libgcrypt20 \
|
||||
libqrencode4 \
|
||||
libxss1 \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& apt-get clean
|
||||
|
||||
COPY --from=builder /usr/local/bin/profanity /usr/local/bin/profanity
|
||||
COPY --from=builder /usr/local/lib/libprofanity* /usr/local/lib/
|
||||
COPY --from=builder /usr/local/include/profapi.h /usr/local/include/
|
||||
COPY --from=builder /usr/local/share/profanity /usr/local/share/profanity
|
||||
COPY --from=builder /usr/lib/libstrophe.so* /usr/lib/
|
||||
|
||||
RUN ldconfig
|
||||
|
||||
RUN ldd /usr/local/bin/profanity | grep 'not found' && exit 1 || true
|
||||
|
||||
RUN mkdir -p /root/.config/profanity
|
||||
|
||||
ENTRYPOINT ["profanity"]
|
||||
@@ -110,6 +110,8 @@ static char* _software_autocomplete(ProfWin* window, const char* const input, gb
|
||||
static char* _url_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _executable_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _lastactivity_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _disco_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _roster_jid_autocomplete(const char* const input, const char* const prefix, gboolean previous);
|
||||
static char* _intype_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _mood_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
static char* _strophe_autocomplete(ProfWin* window, const char* const input, gboolean previous);
|
||||
@@ -122,6 +124,7 @@ static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean
|
||||
static char* _script_autocomplete_func(const char* const prefix, gboolean previous, void* context);
|
||||
|
||||
static char* _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previous);
|
||||
static gboolean _is_connected(void);
|
||||
|
||||
static Autocomplete commands_ac;
|
||||
static Autocomplete who_room_ac;
|
||||
@@ -1426,6 +1429,7 @@ cmd_ac_init(void)
|
||||
g_hash_table_insert(ac_funcs, "/color", _color_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/connect", _connect_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/console", _console_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/disco", _disco_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/correct", _correct_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/correction", _correction_autocomplete);
|
||||
g_hash_table_insert(ac_funcs, "/executable", _executable_autocomplete);
|
||||
@@ -1862,10 +1866,12 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
|
||||
}
|
||||
|
||||
gchar* history_jid_subcmds[] = { "/history verify", "/history export", "/history import" };
|
||||
for (size_t i = 0; i < ARRAY_SIZE(history_jid_subcmds); i++) {
|
||||
result = autocomplete_param_with_func(input, history_jid_subcmds[i], roster_barejid_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
if (conn_status == JABBER_CONNECTED) {
|
||||
for (size_t i = 0; i < ARRAY_SIZE(history_jid_subcmds); i++) {
|
||||
result = autocomplete_param_with_func(input, history_jid_subcmds[i], roster_barejid_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1941,7 +1947,6 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
|
||||
Autocomplete completer;
|
||||
} ac_cmds[] = {
|
||||
{ "/prefs", prefs_ac },
|
||||
{ "/disco", disco_ac },
|
||||
{ "/room", room_ac },
|
||||
{ "/mainwin", winpos_ac },
|
||||
{ "/inputwin", winpos_ac },
|
||||
@@ -1991,6 +1996,31 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static gboolean
|
||||
_is_connected(void)
|
||||
{
|
||||
return connection_get_status() == JABBER_CONNECTED;
|
||||
}
|
||||
|
||||
static char*
|
||||
_roster_jid_autocomplete(const char* const input, const char* const prefix, gboolean previous)
|
||||
{
|
||||
if (!_is_connected()) {
|
||||
return NULL;
|
||||
}
|
||||
char* result = NULL;
|
||||
result = autocomplete_param_with_func(input, prefix, roster_contact_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = autocomplete_param_with_func(input, prefix, roster_barejid_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = autocomplete_param_with_func(input, prefix, roster_fulljid_autocomplete, previous, NULL);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char*
|
||||
_caps_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
@@ -2006,20 +2036,29 @@ _caps_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
if (nick_ac) {
|
||||
result = autocomplete_param_with_ac(input, "/caps", nick_ac, TRUE, previous);
|
||||
}
|
||||
} else if (connection_get_status() == JABBER_CONNECTED) {
|
||||
result = autocomplete_param_with_func(input, "/caps", roster_contact_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = autocomplete_param_with_func(input, "/caps", roster_barejid_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = autocomplete_param_with_func(input, "/caps", roster_fulljid_autocomplete, previous, NULL);
|
||||
} else {
|
||||
result = _roster_jid_autocomplete(input, "/caps", previous);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static char*
|
||||
_disco_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
char* result = NULL;
|
||||
result = autocomplete_param_with_ac(input, "/disco", disco_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = _roster_jid_autocomplete(input, "/disco info", previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = _roster_jid_autocomplete(input, "/disco items", previous);
|
||||
return result;
|
||||
}
|
||||
|
||||
static char*
|
||||
_sub_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
{
|
||||
@@ -3698,7 +3737,7 @@ _win_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
}
|
||||
|
||||
char* unquoted = strip_arg_quotes(input);
|
||||
result = autocomplete_param_with_func(unquoted, "/win", roster_contact_autocomplete, previous, NULL);
|
||||
result = _roster_jid_autocomplete(input, "/win", previous);
|
||||
free(unquoted);
|
||||
return result;
|
||||
}
|
||||
@@ -4032,18 +4071,14 @@ _invite_autocomplete(ProfWin* window, const char* const input, gboolean previous
|
||||
return result;
|
||||
}
|
||||
|
||||
jabber_conn_status_t conn_status = connection_get_status();
|
||||
result = _roster_jid_autocomplete(input, "/invite send", previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (conn_status == JABBER_CONNECTED) {
|
||||
result = autocomplete_param_with_func(input, "/invite send", roster_contact_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_func(input, "/invite decline", muc_invites_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = autocomplete_param_with_func(input, "/invite decline", muc_invites_find, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
@@ -4059,43 +4094,38 @@ _status_autocomplete(ProfWin* window, const char* const input, gboolean previous
|
||||
return result;
|
||||
}
|
||||
|
||||
jabber_conn_status_t conn_status = connection_get_status();
|
||||
// complete with: online, away etc.
|
||||
result = autocomplete_param_with_ac(input, "/status set", account_status_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (conn_status == JABBER_CONNECTED) {
|
||||
// Remove quote character before and after names when doing autocomplete
|
||||
char* unquoted = strip_arg_quotes(input);
|
||||
|
||||
// complete with: online, away etc.
|
||||
result = autocomplete_param_with_ac(input, "/status set", account_status_ac, TRUE, previous);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Remove quote character before and after names when doing autocomplete
|
||||
char* unquoted = strip_arg_quotes(input);
|
||||
|
||||
// MUC completion with nicknames
|
||||
if (window->type == WIN_MUC) {
|
||||
ProfMucWin* mucwin = (ProfMucWin*)window;
|
||||
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
|
||||
Autocomplete nick_ac = muc_roster_ac(mucwin->roomjid);
|
||||
if (nick_ac) {
|
||||
result = autocomplete_param_with_ac(unquoted, "/status get", nick_ac, TRUE, previous);
|
||||
if (result) {
|
||||
free(unquoted);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// roster completion
|
||||
} else {
|
||||
result = autocomplete_param_with_func(unquoted, "/status get", roster_contact_autocomplete, previous, NULL);
|
||||
// MUC completion with nicknames
|
||||
if (window->type == WIN_MUC) {
|
||||
ProfMucWin* mucwin = (ProfMucWin*)window;
|
||||
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
|
||||
Autocomplete nick_ac = muc_roster_ac(mucwin->roomjid);
|
||||
if (nick_ac) {
|
||||
result = autocomplete_param_with_ac(unquoted, "/status get", nick_ac, TRUE, previous);
|
||||
if (result) {
|
||||
free(unquoted);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
free(unquoted);
|
||||
// roster completion
|
||||
} else {
|
||||
result = _roster_jid_autocomplete(input, "/status get", previous);
|
||||
if (result) {
|
||||
free(unquoted);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
free(unquoted);
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -4202,14 +4232,12 @@ _software_autocomplete(ProfWin* window, const char* const input, gboolean previo
|
||||
{
|
||||
char* result = NULL;
|
||||
|
||||
if (window->type == WIN_CHAT) {
|
||||
if (window->type == WIN_CHAT && _is_connected()) {
|
||||
ProfChatWin* chatwin = (ProfChatWin*)window;
|
||||
assert(chatwin->memcheck == PROFCHATWIN_MEMCHECK);
|
||||
|
||||
GString* search_str = g_string_new("/software ");
|
||||
g_string_append(search_str, chatwin->barejid);
|
||||
result = autocomplete_param_with_func(search_str->str, "/software", roster_fulljid_autocomplete, previous, NULL);
|
||||
g_string_free(search_str, TRUE);
|
||||
auto_gchar gchar* search_str = g_strdup_printf("/software %s", chatwin->barejid);
|
||||
result = autocomplete_param_with_func(search_str, "/software", roster_fulljid_autocomplete, previous, NULL);
|
||||
} else if (window->type == WIN_MUC) {
|
||||
ProfMucWin* mucwin = (ProfMucWin*)window;
|
||||
assert(mucwin->memcheck == PROFMUCWIN_MEMCHECK);
|
||||
@@ -4223,10 +4251,7 @@ _software_autocomplete(ProfWin* window, const char* const input, gboolean previo
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result = autocomplete_param_with_func(input, "/software", roster_fulljid_autocomplete, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = _roster_jid_autocomplete(input, "/software", previous);
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -4325,16 +4350,11 @@ _lastactivity_autocomplete(ProfWin* window, const char* const input, gboolean pr
|
||||
return result;
|
||||
}
|
||||
|
||||
jabber_conn_status_t conn_status = connection_get_status();
|
||||
|
||||
if (conn_status == JABBER_CONNECTED) {
|
||||
|
||||
result = autocomplete_param_with_func(input, "/lastactivity set", prefs_autocomplete_boolean_choice, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = autocomplete_param_with_func(input, "/lastactivity get", roster_barejid_autocomplete, previous, NULL);
|
||||
result = autocomplete_param_with_func(input, "/lastactivity set", prefs_autocomplete_boolean_choice, previous, NULL);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
result = _roster_jid_autocomplete(input, "/lastactivity get", previous);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -4514,19 +4534,19 @@ _vcard_autocomplete(ProfWin* window, const char* const input, gboolean previous)
|
||||
} else {
|
||||
char* unquoted = strip_arg_quotes(input);
|
||||
|
||||
result = autocomplete_param_with_func(unquoted, "/vcard get", roster_contact_autocomplete, previous, NULL);
|
||||
result = _roster_jid_autocomplete(input, "/vcard get", previous);
|
||||
if (result) {
|
||||
free(unquoted);
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_func(unquoted, "/vcard photo open", roster_contact_autocomplete, previous, NULL);
|
||||
result = _roster_jid_autocomplete(input, "/vcard photo open", previous);
|
||||
if (result) {
|
||||
free(unquoted);
|
||||
return result;
|
||||
}
|
||||
|
||||
result = autocomplete_param_with_func(unquoted, "/vcard photo save", roster_contact_autocomplete, previous, NULL);
|
||||
result = _roster_jid_autocomplete(input, "/vcard photo save", previous);
|
||||
if (result) {
|
||||
free(unquoted);
|
||||
return result;
|
||||
|
||||
@@ -110,6 +110,15 @@ static gboolean _cmd_execute_default(ProfWin* window, const char* inp);
|
||||
static gboolean _cmd_execute_alias(ProfWin* window, const char* const inp, gboolean* ran);
|
||||
static gboolean
|
||||
_download_install_plugin(ProfWin* window, gchar* url, gchar* path);
|
||||
static const gchar*
|
||||
_resolve_contact_jid(const char* const name)
|
||||
{
|
||||
char* barejid = roster_barejid_from_name(name);
|
||||
if (barejid == NULL) {
|
||||
barejid = (char*)name;
|
||||
}
|
||||
return barejid;
|
||||
}
|
||||
|
||||
static void
|
||||
_vcard_editor_finished_cb(gchar* message, void* user_data)
|
||||
@@ -3414,7 +3423,7 @@ cmd_caps(ProfWin* window, const char* const command, gchar** args)
|
||||
case WIN_CONSOLE:
|
||||
case WIN_XML:
|
||||
if (args[0]) {
|
||||
auto_jid Jid* jid = jid_create(args[0]);
|
||||
auto_jid Jid* jid = jid_create(_resolve_contact_jid(args[0]));
|
||||
|
||||
if (jid == NULL || jid->fulljid == NULL) {
|
||||
cons_show("You must provide a full jid to the /caps command.");
|
||||
@@ -3456,7 +3465,7 @@ cmd_caps(ProfWin* window, const char* const command, gchar** args)
|
||||
}
|
||||
|
||||
static void
|
||||
_send_software_version_iq_to_fulljid(char* request)
|
||||
_send_software_version_iq_to_fulljid(const gchar* const request)
|
||||
{
|
||||
auto_jid Jid* jid = jid_create(request);
|
||||
|
||||
@@ -3497,7 +3506,7 @@ cmd_software(ProfWin* window, const char* const command, gchar** args)
|
||||
break;
|
||||
case WIN_CHAT:
|
||||
if (args[0]) {
|
||||
_send_software_version_iq_to_fulljid(args[0]);
|
||||
_send_software_version_iq_to_fulljid(_resolve_contact_jid(args[0]));
|
||||
break;
|
||||
} else {
|
||||
ProfChatWin* chatwin = (ProfChatWin*)window;
|
||||
@@ -3521,7 +3530,7 @@ cmd_software(ProfWin* window, const char* const command, gchar** args)
|
||||
}
|
||||
case WIN_CONSOLE:
|
||||
if (args[0]) {
|
||||
_send_software_version_iq_to_fulljid(args[0]);
|
||||
_send_software_version_iq_to_fulljid(_resolve_contact_jid(args[0]));
|
||||
} else {
|
||||
cons_show("You must provide a jid to the /software command.");
|
||||
}
|
||||
@@ -4831,7 +4840,7 @@ cmd_disco(ProfWin* window, const char* const command, gchar** args)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
auto_gchar gchar* jid = g_strdup_printf("%s", args[1] ?: connection_get_jid()->domainpart);
|
||||
auto_gchar gchar* jid = g_strdup_printf("%s", args[1] ? _resolve_contact_jid(args[1]) : connection_get_jid()->domainpart);
|
||||
|
||||
if (g_strcmp0(args[0], "info") == 0) {
|
||||
iq_disco_info_request(jid);
|
||||
@@ -5053,7 +5062,7 @@ cmd_lastactivity(ProfWin* window, const char* const command, gchar** args)
|
||||
if (args[1] == NULL) {
|
||||
iq_last_activity_request(connection_get_jid()->domainpart);
|
||||
} else {
|
||||
iq_last_activity_request(args[1]);
|
||||
iq_last_activity_request(_resolve_contact_jid(args[1]));
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -6406,17 +6415,17 @@ cmd_ping(ProfWin* window, const char* const command, gchar** args)
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if (args[0] != NULL && caps_jid_has_feature(args[0], XMPP_FEATURE_PING) == FALSE) {
|
||||
cons_show("%s does not support ping requests.", args[0]);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
iq_send_ping(args[0]);
|
||||
|
||||
if (args[0] == NULL) {
|
||||
cons_show("Pinged server…");
|
||||
if (args[0] != NULL) {
|
||||
const gchar* ping_target = _resolve_contact_jid(args[0]);
|
||||
if (caps_jid_has_feature(ping_target, XMPP_FEATURE_PING) == FALSE) {
|
||||
cons_show("%s does not support ping requests.", ping_target);
|
||||
return TRUE;
|
||||
}
|
||||
iq_send_ping(ping_target);
|
||||
cons_show("Pinged %s…", ping_target);
|
||||
} else {
|
||||
cons_show("Pinged %s…", args[0]);
|
||||
iq_send_ping(NULL);
|
||||
cons_show("Pinged server…");
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
26
src/common.c
26
src/common.c
@@ -455,32 +455,6 @@ 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)
|
||||
{
|
||||
|
||||
@@ -161,7 +161,6 @@ 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,7 +39,6 @@
|
||||
#include <sys/statvfs.h>
|
||||
#include <sqlite3.h>
|
||||
#include <glib.h>
|
||||
#include <glib/gstdio.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -125,15 +124,6 @@ _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)
|
||||
{
|
||||
@@ -159,20 +149,6 @@ _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();
|
||||
@@ -715,7 +691,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.", from_jid->barejid, from_jid_orig, message->replace_id);
|
||||
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);
|
||||
cons_show_error("%s sent a message correction with mismatched sender. See log for details.", from_jid->barejid);
|
||||
sqlite3_finalize(lmc_stmt);
|
||||
return;
|
||||
@@ -738,7 +714,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", message->id, message->stanzaid, from_jid->barejid);
|
||||
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);
|
||||
cons_show_error("Got a message with duplicate (server-generated) stanza-id from %s.", from_jid->fulljid);
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -769,7 +745,7 @@ _add_to_db(ProfMessage* message, const char* type, const Jid* const from_jid, co
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
log_debug("Writing to DB. Query: %s", query);
|
||||
|
||||
if (SQLITE_OK != sqlite3_exec(g_chatlog_database, query, NULL, 0, &err_msg)) {
|
||||
if (err_msg) {
|
||||
@@ -781,7 +757,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)", inserted_rows_count, message->id);
|
||||
log_error("SQLite did not insert message (rows: %d, id: %s, content: %s)", inserted_rows_count, message->id, message->plain);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ 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();
|
||||
|
||||
@@ -286,8 +286,7 @@ log_stderr_handler(void)
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
if (buf[i] == '\n') {
|
||||
auto_gchar gchar* redacted = redact_secrets(s->str); // third-party libs may echo credentials
|
||||
log_msg(stderr_level, "stderr", redacted);
|
||||
log_msg(stderr_level, "stderr", s->str);
|
||||
g_string_assign(s, "");
|
||||
} else
|
||||
g_string_append_c(s, buf[i]);
|
||||
@@ -295,8 +294,7 @@ log_stderr_handler(void)
|
||||
} while (1);
|
||||
|
||||
if (s->len > 0 && s->str[0] != '\0') {
|
||||
auto_gchar gchar* redacted = redact_secrets(s->str);
|
||||
log_msg(stderr_level, "stderr", redacted);
|
||||
log_msg(stderr_level, "stderr", s->str);
|
||||
g_string_assign(s, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
#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"
|
||||
@@ -116,8 +114,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -380,7 +376,6 @@ 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,7 +390,6 @@ 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,12 +13,10 @@
|
||||
#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>
|
||||
@@ -64,28 +62,14 @@ aesgcm_file_get(void* userdata)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// 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");
|
||||
// Open the target file for storing the cleartext.
|
||||
auto_FILE FILE* outfh = fopen(aesgcm_dl->filename, "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;
|
||||
}
|
||||
|
||||
@@ -106,8 +90,6 @@ 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;
|
||||
@@ -120,8 +102,6 @@ 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;
|
||||
}
|
||||
|
||||
@@ -130,32 +110,20 @@ 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);
|
||||
}
|
||||
|
||||
// never hand an unverified file to the external command
|
||||
if (saved && aesgcm_dl->cmd_template != NULL) {
|
||||
if (aesgcm_dl->cmd_template != NULL) {
|
||||
gchar** argv = format_call_external_argv(aesgcm_dl->cmd_template,
|
||||
aesgcm_dl->filename,
|
||||
aesgcm_dl->filename);
|
||||
@@ -172,8 +140,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);
|
||||
|
||||
@@ -1104,8 +1104,7 @@ _xmpp_file_logger(void* const userdata, const xmpp_log_level_t xmpp_level, const
|
||||
break;
|
||||
}
|
||||
|
||||
auto_gchar gchar* redacted = redact_secrets(msg); // raw traffic contains SASL/register credentials
|
||||
log_msg(prof_level, area, redacted);
|
||||
log_msg(prof_level, area, msg);
|
||||
|
||||
if ((g_strcmp0(area, "xmpp") == 0) || (g_strcmp0(area, "conn")) == 0) {
|
||||
sv_ev_xmpp_stanza(msg);
|
||||
|
||||
@@ -255,9 +255,6 @@ 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),
|
||||
|
||||
@@ -536,41 +536,3 @@ 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,4 +11,3 @@ 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);
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
/*
|
||||
* 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
|
||||
@@ -1,8 +0,0 @@
|
||||
/* 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);
|
||||
@@ -1384,55 +1384,3 @@ 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,5 @@ 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 redact_secrets__masks_credentials(void** state);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -48,7 +48,6 @@
|
||||
#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)
|
||||
|
||||
@@ -688,12 +687,6 @@ 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(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