branch: elpa/jabber
commit ff43627ae685e7a84b9be814b2fabed002f0c052
Author: Thanos Apollo <[email protected]>
Commit: Thanos Apollo <[email protected]>
omemo: Make session migration transaction-safe
---
lisp/jabber-omemo-store.el | 74 ++---
lisp/jabber-omemo.el | 83 +-----
src/jabber-omemo-core.c | 564 +++++++++++++++++++++++++++---------
src/picomemo/omemo.c | 23 +-
tests/jabber-test-mam.el | 34 +++
tests/jabber-test-omemo-message.el | 3 +-
tests/jabber-test-omemo-module.el | 158 +++++++++-
tests/jabber-test-omemo-protocol.el | 53 +++-
tests/jabber-test-omemo-store.el | 106 ++++---
9 files changed, 768 insertions(+), 330 deletions(-)
diff --git a/lisp/jabber-omemo-store.el b/lisp/jabber-omemo-store.el
index 929f4d5591..50571b6a5b 100644
--- a/lisp/jabber-omemo-store.el
+++ b/lisp/jabber-omemo-store.el
@@ -245,6 +245,31 @@ INSERT OR REPLACE INTO omemo_sessions (account, jid,
device_id, session_blob)
VALUES (?, ?, ?, ?)"
(list account jid device-id blob))))
+(defun jabber-omemo-store-save-session-and-clear-legacy-keys
+ (account jid device-id blob)
+ "Save session BLOB for ACCOUNT, JID, DEVICE-ID and remove legacy keys."
+ (when-let* ((db (jabber-db-ensure-open)))
+ (sqlite-execute db "SAVEPOINT omemo_session_migration")
+ (condition-case err
+ (prog1
+ (progn
+ (jabber-omemo-store-save-session account jid device-id blob)
+ (jabber-omemo-store-delete-skipped-keys
+ account jid device-id))
+ (sqlite-execute db "RELEASE omemo_session_migration"))
+ (error
+ (condition-case cleanup-err
+ (sqlite-execute db "ROLLBACK TO omemo_session_migration")
+ (error
+ (message "OMEMO session rollback failed: %s"
+ (error-message-string cleanup-err))))
+ (condition-case cleanup-err
+ (sqlite-execute db "RELEASE omemo_session_migration")
+ (error
+ (message "OMEMO session savepoint release failed: %s"
+ (error-message-string cleanup-err))))
+ (signal (car err) (cdr err))))))
+
(defun jabber-omemo-store-load-session (account jid device-id)
"Load session blob for ACCOUNT, JID, DEVICE-ID, or nil."
(when-let* ((db (jabber-db-ensure-open)))
@@ -274,42 +299,7 @@ SELECT device_id, session_blob FROM omemo_sessions
WHERE account = ? AND jid = ?"
(list account jid)))))
-;;; Skipped key CRUD
-
-(defun jabber-omemo-store-save-skipped-key (account jid device-id
- dh-key msg-number msg-key)
- "Store a skipped message key for ACCOUNT, JID, DEVICE-ID.
-DH-KEY and MSG-KEY are unibyte blobs. MSG-NUMBER is an integer."
- (when-let* ((db (jabber-db-ensure-open)))
- (let ((now (truncate (float-time))))
- (sqlite-execute db "\
-INSERT OR REPLACE INTO omemo_skipped_keys
- (account, jid, device_id, dh_key, message_number, message_key, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?)"
- (list account jid device-id dh-key msg-number msg-key
now)))))
-
-(defun jabber-omemo-store-load-skipped-key (account jid device-id
- dh-key msg-number)
- "Load a skipped message key for ACCOUNT+JID+DEVICE-ID, or nil.
-DH-KEY identifies the ratchet step and MSG-NUMBER the message within it."
- (when-let* ((db (jabber-db-ensure-open)))
- (jabber-omemo-store--as-unibyte
- (caar (sqlite-select db "\
-SELECT message_key FROM omemo_skipped_keys
- WHERE account = ? AND jid = ? AND device_id = ?
- AND dh_key = ? AND message_number = ?"
- (list account jid device-id dh-key msg-number))))))
-
-(defun jabber-omemo-store-delete-skipped-key (account jid device-id
- dh-key msg-number)
- "Delete a skipped message key for ACCOUNT+JID+DEVICE-ID after use.
-DH-KEY and MSG-NUMBER identify the entry."
- (when-let* ((db (jabber-db-ensure-open)))
- (sqlite-execute db "\
-DELETE FROM omemo_skipped_keys
- WHERE account = ? AND jid = ? AND device_id = ?
- AND dh_key = ? AND message_number = ?"
- (list account jid device-id dh-key msg-number))))
+;;; Legacy skipped-key migration
(defun jabber-omemo-store-all-skipped-keys (account jid device-id)
"Return all skipped message keys for ACCOUNT+JID+DEVICE-ID.
@@ -322,7 +312,8 @@ Each element is (MSG-NUMBER DH-KEY MSG-KEY), the entry
format of
(jabber-omemo-store--as-unibyte (caddr row))))
(sqlite-select db "\
SELECT message_number, dh_key, message_key FROM omemo_skipped_keys
- WHERE account = ? AND jid = ? AND device_id = ?"
+ WHERE account = ? AND jid = ? AND device_id = ?
+ ORDER BY created_at ASC, rowid ASC"
(list account jid device-id)))))
(defun jabber-omemo-store-delete-skipped-keys (account jid device-id)
@@ -335,14 +326,5 @@ DELETE FROM omemo_skipped_keys
WHERE account = ? AND jid = ? AND device_id = ?"
(list account jid device-id))))
-(defun jabber-omemo-store-delete-old-skipped-keys (account max-age)
- "Delete skipped keys for ACCOUNT older than MAX-AGE seconds."
- (when-let* ((db (jabber-db-ensure-open)))
- (let ((cutoff (- (truncate (float-time)) max-age)))
- (sqlite-execute db "\
-DELETE FROM omemo_skipped_keys
- WHERE account = ? AND created_at < ?"
- (list account cutoff)))))
-
(provide 'jabber-omemo-store)
;;; jabber-omemo-store.el ends here
diff --git a/lisp/jabber-omemo.el b/lisp/jabber-omemo.el
index 758d540fbb..c1caa86d6b 100644
--- a/lisp/jabber-omemo.el
+++ b/lisp/jabber-omemo.el
@@ -63,12 +63,6 @@ compile jabber-omemo-core."
:type 'boolean
:group 'jabber)
-(defcustom jabber-omemo-skipped-key-max-age (* 30 86400)
- "Maximum age in seconds for OMEMO skipped message keys.
-Keys older than this are deleted on connect."
- :type 'integer
- :group 'jabber)
-
(defcustom jabber-omemo-signed-pre-key-rotation-period (* 7 86400)
"Seconds between OMEMO signed pre-key rotations.
Checked on connect. XEP-0384 recommends rotating once a week to
@@ -131,6 +125,7 @@ Signal a `user-error' otherwise."
(declare-function jabber-omemo--initiate-session "ext:jabber-omemo-core")
(declare-function jabber-omemo--serialize-session "ext:jabber-omemo-core")
(declare-function jabber-omemo--deserialize-session "ext:jabber-omemo-core")
+(declare-function jabber-omemo--legacy-session-blob-p "ext:jabber-omemo-core")
(declare-function jabber-omemo--encrypt-key "ext:jabber-omemo-core")
(declare-function jabber-omemo--decrypt-key "ext:jabber-omemo-core")
(declare-function jabber-omemo--session-skipped-keys "ext:jabber-omemo-core")
@@ -405,6 +400,11 @@ Returns a deserialized session user-ptr, or nil."
(when-let* ((blob (jabber-omemo-store-load-session
account jid device-id)))
(let ((session-ptr (jabber-omemo-deserialize-session blob)))
+ (when (jabber-omemo--legacy-session-blob-p blob)
+ (jabber-omemo--session-set-skipped-keys
+ session-ptr
+ (jabber-omemo-store-all-skipped-keys
+ account jid device-id)))
(puthash key session-ptr jabber-omemo--sessions)
session-ptr)))))
@@ -414,7 +414,8 @@ Updates both the database and in-memory cache."
(let* ((account (jabber-connection-bare-jid jc))
(key (jabber-omemo--session-key account jid device-id))
(blob (jabber-omemo-serialize-session session-ptr)))
- (jabber-omemo-store-save-session account jid device-id blob)
+ (jabber-omemo-store-save-session-and-clear-legacy-keys
+ account jid device-id blob)
(puthash key session-ptr jabber-omemo--sessions)))
;;; Device list XML helpers
@@ -1113,55 +1114,6 @@ For MUC messages (type=groupchat), try in order:
(or (and real-jid (jabber-jid-user real-jid))
(jabber-omemo--match-jid-by-affiliation group nick))))))
-(defun jabber-omemo--skipped-keys-supported-p ()
- "Return non-nil when the loaded module handles skipped message keys.
-A stale jabber-omemo-core.so lacks the accessors; degrade to the
-old no-recovery behavior instead of erroring."
- (fboundp 'jabber-omemo--session-set-skipped-keys))
-
-(defun jabber-omemo--preload-skipped-keys (jc jid device-id session-ptr)
- "Seed SESSION-PTR with persisted skipped keys for JC/JID/DEVICE-ID.
-Also discards phantom keys left in memory by a failed decrypt.
-Return the seeded list, the before-image for
-`jabber-omemo--sync-skipped-keys'."
- (when (jabber-omemo--skipped-keys-supported-p)
- (let ((keys (jabber-omemo-store-all-skipped-keys
- (jabber-connection-bare-jid jc) jid device-id)))
- (jabber-omemo--session-set-skipped-keys session-ptr keys)
- keys)))
-
-(defun jabber-omemo--skipped-key-changes (before after)
- "Return (NEW . CONSUMED) between skipped-key lists BEFORE and AFTER.
-Keys are (NR DH MK) triples; NR plus DH identifies an entry."
- (let ((id (lambda (k) (cons (car k) (cadr k)))))
- (cons (cl-set-difference after before :key id :test #'equal)
- (cl-set-difference before after :key id :test #'equal))))
-
-(defun jabber-omemo--sync-skipped-keys (jc jid device-id session-ptr before)
- "Persist SESSION-PTR's skipped-key changes since BEFORE to the database."
- (when (jabber-omemo--skipped-keys-supported-p)
- (pcase-let* ((account (jabber-connection-bare-jid jc))
- (`(,new . ,consumed)
- (jabber-omemo--skipped-key-changes
- before (jabber-omemo--session-skipped-keys session-ptr))))
- (dolist (k new)
- (jabber-omemo-store-save-skipped-key
- account jid device-id (cadr k) (car k) (caddr k)))
- (dolist (k consumed)
- (jabber-omemo-store-delete-skipped-key
- account jid device-id (cadr k) (car k))))))
-
-(defun jabber-omemo--reset-skipped-keys (jc jid device-id session-ptr)
- "Replace persisted skipped keys for JC/JID/DEVICE-ID with SESSION-PTR's.
-Used when a fresh session replaces an established one; keys from
-the old ratchet can never match again."
- (when (jabber-omemo--skipped-keys-supported-p)
- (let ((account (jabber-connection-bare-jid jc)))
- (jabber-omemo-store-delete-skipped-keys account jid device-id)
- (dolist (k (jabber-omemo--session-skipped-keys session-ptr))
- (jabber-omemo-store-save-skipped-key
- account jid device-id (cadr k) (car k) (caddr k))))))
-
(defun jabber-omemo--decrypt-key-with-session (jc sender-jid sender-did
store-ptr pre-key-p key-data)
"Decrypt KEY-DATA from SENDER-JID's device SENDER-DID via JC.
@@ -1175,22 +1127,16 @@ the retry is safe. This also resolves a peer that
reset their
session and simultaneous initiations. A regular message requires
an existing session.
-Skipped ratchet keys are seeded from the database before the
-decrypt and changes are persisted after it, so out-of-order
-messages decrypt across restarts.
+Skipped ratchet keys are owned by the native session and persisted
+inside its serialized blob, so out-of-order messages survive restarts.
Returns (SESSION-PTR DECRYPTED-KEY FRESH-P), FRESH-P non-nil when
the fresh-session pre-key path was used. Signals
`jabber-omemo-no-session' or `jabber-omemo-prekey-failed'."
- (let* ((existing (jabber-omemo--get-session jc sender-jid sender-did))
- (before (and existing
- (jabber-omemo--preload-skipped-keys
- jc sender-jid sender-did existing))))
+ (let ((existing (jabber-omemo--get-session jc sender-jid sender-did)))
(cl-flet ((decrypt-existing (prekey)
(let ((key (jabber-omemo-decrypt-key
existing store-ptr prekey key-data)))
- (jabber-omemo--sync-skipped-keys
- jc sender-jid sender-did existing before)
(list existing key nil))))
(cond
((not pre-key-p)
@@ -1206,8 +1152,6 @@ the fresh-session pre-key path was used. Signals
(condition-case err
(let ((key (jabber-omemo-decrypt-key
fresh store-ptr t key-data)))
- (jabber-omemo--reset-skipped-keys
- jc sender-jid sender-did fresh)
(list fresh key t))
(jabber-omemo-error
(signal 'jabber-omemo-prekey-failed
@@ -1628,10 +1572,7 @@ of date, and pre-fetches sessions for open chat buffers."
(jabber-omemo--maybe-rotate-signed-pre-key jc)
(jabber-omemo--ensure-device-listed jc)
(jabber-omemo--publish-bundle-if-needed jc)
- (jabber-omemo--prefetch-open-chats jc)
- (jabber-omemo-store-delete-old-skipped-keys
- (jabber-connection-bare-jid jc)
- jabber-omemo-skipped-key-max-age))
+ (jabber-omemo--prefetch-open-chats jc))
(defun jabber-omemo--prefetch-open-chats (jc)
"Pre-fetch OMEMO sessions for all open OMEMO chat buffers on JC."
diff --git a/src/jabber-omemo-core.c b/src/jabber-omemo-core.c
index c126332558..2e3bd8e490 100644
--- a/src/jabber-omemo-core.c
+++ b/src/jabber-omemo-core.c
@@ -40,10 +40,8 @@ int omemoRandom(void *p, size_t n)
omemoStoreMessageKey and asks for them back in omemoLoadMessageKey.
Both fire synchronously inside omemoDecryptKey, where no emacs_env
is available, so keys live in a malloc'd per-session list here.
- Elisp seeds and drains the list via
- jabber-omemo--session-set-skipped-keys and
- jabber-omemo--session-skipped-keys around each decrypt and
- persists the result in SQLite. */
+ The list is serialized with the native session so skipped keys
+ survive restarts without separate database writes. */
struct skipped_key {
uint32_t nr;
@@ -51,10 +49,11 @@ struct skipped_key {
uint8_t mk[32];
};
-struct session_skipped {
- struct omemoSession *session;
+struct native_session {
+ struct omemoSession session;
struct skipped_key *keys;
size_t count, cap;
+ size_t decrypt_new_count;
};
/* Upper bound on retained skipped keys per session; a peer jumping
@@ -62,8 +61,11 @@ struct session_skipped {
OMEMO_ESTORE instead of allocating without limit. */
#define SKIPPED_KEYS_MAX 1000
-static struct session_skipped *g_skipped;
-static size_t g_skipped_count, g_skipped_cap;
+#define SESSION_MAGIC "JOMEMO\0\1"
+#define SESSION_MAGIC_SIZE 8
+#define SESSION_VERSION 1
+#define SESSION_HEADER_SIZE 20
+#define SKIPPED_KEY_SIZE 68
static void
skipped_clear(void *ptr, size_t size)
@@ -73,74 +75,58 @@ skipped_clear(void *ptr, size_t size)
*p++ = 0;
}
-static struct session_skipped *
-skipped_find(struct omemoSession *s, int create)
+static struct native_session *
+native_session(struct omemoSession *session)
{
- for (size_t i = 0; i < g_skipped_count; i++)
- if (g_skipped[i].session == s)
- return &g_skipped[i];
- if (!create)
- return NULL;
- if (g_skipped_count == g_skipped_cap) {
- size_t ncap = g_skipped_cap ? g_skipped_cap * 2 : 8;
- struct session_skipped *n = realloc(g_skipped, ncap * sizeof *n);
- if (!n)
- return NULL;
- g_skipped = n;
- g_skipped_cap = ncap;
- }
- struct session_skipped *e = &g_skipped[g_skipped_count++];
- e->session = s;
- e->keys = NULL;
- e->count = e->cap = 0;
- return e;
+ return (struct native_session *)session;
}
static void
-skipped_drop(struct omemoSession *s)
+skipped_drop(struct native_session *session)
{
- for (size_t i = 0; i < g_skipped_count; i++) {
- if (g_skipped[i].session == s) {
- if (g_skipped[i].keys) {
- skipped_clear(g_skipped[i].keys,
- g_skipped[i].cap * sizeof(struct skipped_key));
- free(g_skipped[i].keys);
- }
- g_skipped[i] = g_skipped[--g_skipped_count];
- return;
- }
+ if (session->keys) {
+ skipped_clear(session->keys,
+ session->cap * sizeof(struct skipped_key));
+ free(session->keys);
}
+ session->keys = NULL;
+ session->count = session->cap = 0;
}
static int
-skipped_add(struct session_skipped *e, uint32_t nr,
+skipped_add(struct native_session *session, uint32_t nr,
const uint8_t *dh, const uint8_t *mk)
{
- if (e->count >= SKIPPED_KEYS_MAX)
- return 1;
- if (e->count == e->cap) {
- size_t ncap = e->cap ? e->cap * 2 : 16;
- struct skipped_key *n = realloc(e->keys, ncap * sizeof *n);
+ if (session->count == SKIPPED_KEYS_MAX) {
+ skipped_clear(&session->keys[0], sizeof session->keys[0]);
+ memmove(&session->keys[0], &session->keys[1],
+ (session->count - 1) * sizeof session->keys[0]);
+ session->count--;
+ }
+ if (session->count == session->cap) {
+ size_t ncap = session->cap ? session->cap * 2 : 16;
+ if (ncap > SKIPPED_KEYS_MAX)
+ ncap = SKIPPED_KEYS_MAX;
+ struct skipped_key *n = realloc(session->keys, ncap * sizeof *n);
if (!n)
return 1;
- memset(n + e->cap, 0, (ncap - e->cap) * sizeof *n);
- e->keys = n;
- e->cap = ncap;
- }
- e->keys[e->count].nr = nr;
- memcpy(e->keys[e->count].dh, dh, 32);
- memcpy(e->keys[e->count].mk, mk, 32);
- e->count++;
+ memset(n + session->cap, 0,
+ (ncap - session->cap) * sizeof *n);
+ session->keys = n;
+ session->cap = ncap;
+ }
+ session->keys[session->count].nr = nr;
+ memcpy(session->keys[session->count].dh, dh, 32);
+ memcpy(session->keys[session->count].mk, mk, 32);
+ session->count++;
return 0;
}
int omemoLoadMessageKey(struct omemoSession *s, struct omemoMessageKey *k)
{
- struct session_skipped *e = skipped_find(s, 0);
- if (!e)
- return 1; /* not found */
- for (size_t i = 0; i < e->count; i++) {
- struct skipped_key *sk = &e->keys[i];
+ struct native_session *session = native_session(s);
+ for (size_t i = 0; i < session->count; i++) {
+ struct skipped_key *sk = &session->keys[i];
if (sk->nr == k->nr && !memcmp(sk->dh, k->dh, 32)) {
memcpy(k->mk, sk->mk, 32);
return 0;
@@ -152,16 +138,16 @@ int omemoLoadMessageKey(struct omemoSession *s, struct
omemoMessageKey *k)
int omemoRemoveMessageKey(struct omemoSession *s,
const struct omemoMessageKey *k)
{
- struct session_skipped *e = skipped_find(s, 0);
- if (!e)
- return OMEMO_ESTORE;
- for (size_t i = 0; i < e->count; i++) {
- struct skipped_key *sk = &e->keys[i];
+ struct native_session *session = native_session(s);
+ for (size_t i = 0; i < session->count; i++) {
+ struct skipped_key *sk = &session->keys[i];
if (sk->nr == k->nr && !memcmp(sk->dh, k->dh, 32)) {
- e->keys[i] = e->keys[e->count - 1];
- skipped_clear(&e->keys[e->count - 1],
+ skipped_clear(sk, sizeof *sk);
+ memmove(sk, sk + 1,
+ (session->count - i - 1) * sizeof *sk);
+ skipped_clear(&session->keys[session->count - 1],
sizeof(struct skipped_key));
- e->count--;
+ session->count--;
return 0;
}
}
@@ -172,9 +158,11 @@ int omemoStoreMessageKey(struct omemoSession *s,
const struct omemoMessageKey *k, uint64_t n)
{
(void)n;
- struct session_skipped *e = skipped_find(s, 1);
- if (!e || skipped_add(e, k->nr, k->dh, k->mk))
+ struct native_session *session = native_session(s);
+ if (session->decrypt_new_count >= SKIPPED_KEYS_MAX ||
+ skipped_add(session, k->nr, k->dh, k->mk))
return OMEMO_ESTORE;
+ session->decrypt_new_count++;
return 0;
}
@@ -206,7 +194,9 @@ provide(emacs_env *env, const char *feature)
static void
signal_error(emacs_env *env, int code, const char *msg)
{
- emacs_value data = env->make_string(env, msg, strlen(msg));
+ emacs_value message = env->make_string(env, msg, strlen(msg));
+ emacs_value data = env->funcall(env, env->intern(env, "list"),
+ 1, &message);
emacs_value errsym = Qjabber_omemo_error;
env->non_local_exit_signal(env, errsym, data);
(void)code;
@@ -231,6 +221,21 @@ extract_unibyte(emacs_env *env, emacs_value arg,
return 0;
}
+static int
+extract_exact_unibyte(emacs_env *env, emacs_value arg, uint8_t *buf,
+ size_t expected, const char *message)
+{
+ ptrdiff_t len = 0;
+ env->copy_string_contents(env, arg, NULL, &len);
+ if (env->non_local_exit_check(env))
+ return -1;
+ if (len != (ptrdiff_t)expected + 1) {
+ signal_error(env, OMEMO_ESTORE, message);
+ return -1;
+ }
+ return extract_unibyte(env, arg, buf, expected + 1, NULL);
+}
+
/* Finalizers for user-ptr */
static void
@@ -242,10 +247,63 @@ free_store(void *ptr)
static void
free_session(void *ptr)
{
- skipped_drop(ptr);
+ struct native_session *session = ptr;
+ skipped_drop(session);
+ skipped_clear(&session->session, sizeof session->session);
free(ptr);
}
+static void *
+checked_user_ptr(emacs_env *env, emacs_value value,
+ void (*expected)(void *), const char *message)
+{
+ void (*finalizer)(void *) = env->get_user_finalizer(env, value);
+ if (env->non_local_exit_check(env))
+ return NULL;
+ if (finalizer != expected) {
+ signal_error(env, OMEMO_ESTORE, message);
+ return NULL;
+ }
+ return env->get_user_ptr(env, value);
+}
+
+static struct omemoStore *
+extract_store(emacs_env *env, emacs_value value)
+{
+ return checked_user_ptr(env, value, free_store,
+ "expected an OMEMO store pointer");
+}
+
+static struct native_session *
+extract_session(emacs_env *env, emacs_value value)
+{
+ return checked_user_ptr(env, value, free_session,
+ "expected an OMEMO session pointer");
+}
+
+static uint32_t
+read_u32(const uint8_t *p)
+{
+ return ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
+ ((uint32_t)p[2] << 8) | p[3];
+}
+
+static void
+write_u32(uint8_t *p, uint32_t value)
+{
+ p[0] = value >> 24;
+ p[1] = value >> 16;
+ p[2] = value >> 8;
+ p[3] = value;
+}
+
+static bool
+session_envelope_p(const uint8_t *blob, size_t len)
+{
+ return len >= SESSION_MAGIC_SIZE &&
+ !memcmp(blob, SESSION_MAGIC, SESSION_MAGIC_SIZE);
+}
+
/* jabber-omemo--setup-store */
static emacs_value
@@ -327,7 +385,7 @@ F_serialize_store(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoStore *store = env->get_user_ptr(env, args[0]);
+ struct omemoStore *store = extract_store(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
@@ -352,7 +410,7 @@ F_get_bundle(emacs_env *env, ptrdiff_t nargs, emacs_value
*args,
{
(void)nargs; (void)data;
- struct omemoStore *store = env->get_user_ptr(env, args[0]);
+ struct omemoStore *store = extract_store(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
@@ -426,7 +484,7 @@ F_rotate_signed_pre_key(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoStore *store = env->get_user_ptr(env, args[0]);
+ struct omemoStore *store = extract_store(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
@@ -446,7 +504,7 @@ F_refill_pre_keys(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoStore *store = env->get_user_ptr(env, args[0]);
+ struct omemoStore *store = extract_store(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
@@ -466,7 +524,7 @@ F_remove_pre_key(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoStore *store = env->get_user_ptr(env, args[0]);
+ struct omemoStore *store = extract_store(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
@@ -494,11 +552,11 @@ F_used_pre_key_id(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoSession *session = env->get_user_ptr(env, args[0]);
+ struct native_session *native = extract_session(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
- return env->make_integer(env, session->usedpk_id);
+ return env->make_integer(env, native->session.usedpk_id);
}
/* jabber-omemo--encrypt-message */
@@ -567,15 +625,15 @@ F_decrypt_message(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
(void)nargs; (void)data;
/* Extract key (32 bytes) */
- uint8_t key[33]; /* +1 for NUL from copy_string_contents */
- size_t keylen;
- if (extract_unibyte(env, args[0], key, sizeof(key), &keylen))
+ uint8_t key[33];
+ if (extract_exact_unibyte(env, args[0], key, 32,
+ "message key must be exactly 32 bytes"))
return Qnil_v;
/* Extract IV (12 bytes) */
uint8_t iv[13];
- size_t ivlen;
- if (extract_unibyte(env, args[1], iv, sizeof(iv), &ivlen))
+ if (extract_exact_unibyte(env, args[1], iv, 12,
+ "message IV must be exactly 12 bytes"))
return Qnil_v;
/* Extract ciphertext */
@@ -603,7 +661,7 @@ F_decrypt_message(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
return Qnil_v;
}
- int rc = omemoDecryptMessage(plaintext, key, keylen, iv, ciphertext,
+ int rc = omemoDecryptMessage(plaintext, key, 32, iv, ciphertext,
ctlen);
free(ciphertext);
if (rc) {
@@ -625,7 +683,7 @@ F_make_session(emacs_env *env, ptrdiff_t nargs, emacs_value
*args,
{
(void)nargs; (void)args; (void)data;
- struct omemoSession *session = calloc(1, sizeof(*session));
+ struct native_session *session = calloc(1, sizeof(*session));
if (!session) {
signal_error(env, -1, "calloc failed");
return Qnil_v;
@@ -642,28 +700,32 @@ F_initiate_session(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoStore *store = env->get_user_ptr(env, args[0]);
+ struct omemoStore *store = extract_store(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
/* Extract signature (64 bytes) */
uint8_t sig[65];
- if (extract_unibyte(env, args[1], sig, sizeof(sig), NULL))
+ if (extract_exact_unibyte(env, args[1], sig, 64,
+ "signature must be exactly 64 bytes"))
return Qnil_v;
/* Extract signed pre-key (33 bytes) */
uint8_t spk[34];
- if (extract_unibyte(env, args[2], spk, sizeof(spk), NULL))
+ if (extract_exact_unibyte(env, args[2], spk, 33,
+ "signed pre-key must be exactly 33 bytes"))
return Qnil_v;
/* Extract identity key (33 bytes) */
uint8_t ik[34];
- if (extract_unibyte(env, args[3], ik, sizeof(ik), NULL))
+ if (extract_exact_unibyte(env, args[3], ik, 33,
+ "identity key must be exactly 33 bytes"))
return Qnil_v;
/* Extract pre-key (33 bytes) */
uint8_t pk[34];
- if (extract_unibyte(env, args[4], pk, sizeof(pk), NULL))
+ if (extract_exact_unibyte(env, args[4], pk, 33,
+ "pre-key must be exactly 33 bytes"))
return Qnil_v;
uint32_t spk_id = (uint32_t)env->extract_integer(env, args[5]);
@@ -674,13 +736,13 @@ F_initiate_session(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
if (env->non_local_exit_check(env))
return Qnil_v;
- struct omemoSession *session = calloc(1, sizeof(*session));
+ struct native_session *session = calloc(1, sizeof(*session));
if (!session) {
signal_error(env, -1, "calloc failed");
return Qnil_v;
}
- int rc = omemoInitiateSession(session, store, sig, spk, ik, pk,
+ int rc = omemoInitiateSession(&session->session, store, sig, spk, ik, pk,
spk_id, pk_id);
if (rc) {
free(session);
@@ -699,19 +761,39 @@ F_serialize_session(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoSession *session = env->get_user_ptr(env, args[0]);
+ struct native_session *session = extract_session(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
- size_t sz = omemoGetSerializedSessionSize(session);
+ size_t raw_size = omemoGetSerializedSessionSize(&session->session);
+ if (session->count > SKIPPED_KEYS_MAX ||
+ raw_size > UINT32_MAX ||
+ session->count > (SIZE_MAX - SESSION_HEADER_SIZE - raw_size) /
+ SKIPPED_KEY_SIZE) {
+ signal_error(env, OMEMO_ESTORE, "session is too large to serialize");
+ return Qnil_v;
+ }
+ size_t sz = SESSION_HEADER_SIZE + raw_size +
+ session->count * SKIPPED_KEY_SIZE;
uint8_t *buf = malloc(sz);
if (!buf) {
signal_error(env, -1, "malloc failed");
return Qnil_v;
}
- omemoSerializeSession(buf, session);
+ memcpy(buf, SESSION_MAGIC, SESSION_MAGIC_SIZE);
+ write_u32(buf + 8, SESSION_VERSION);
+ write_u32(buf + 12, (uint32_t)raw_size);
+ write_u32(buf + 16, (uint32_t)session->count);
+ omemoSerializeSession(buf + SESSION_HEADER_SIZE, &session->session);
+ uint8_t *p = buf + SESSION_HEADER_SIZE + raw_size;
+ for (size_t i = 0; i < session->count; i++, p += SKIPPED_KEY_SIZE) {
+ write_u32(p, session->keys[i].nr);
+ memcpy(p + 4, session->keys[i].dh, 32);
+ memcpy(p + 36, session->keys[i].mk, 32);
+ }
emacs_value result = make_unibyte(env, buf, sz);
+ skipped_clear(buf, sz);
free(buf);
return result;
}
@@ -741,16 +823,53 @@ F_deserialize_session(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
}
size_t datalen = (size_t)(bloblen - 1);
- struct omemoSession *session = calloc(1, sizeof(*session));
+ struct native_session *session = calloc(1, sizeof(*session));
if (!session) {
free(blob);
signal_error(env, -1, "calloc failed");
return Qnil_v;
}
- int rc = omemoDeserializeSession(blob, datalen, session);
+ const uint8_t *raw = blob;
+ size_t raw_size = datalen;
+ uint32_t key_count = 0;
+ if (session_envelope_p(blob, datalen)) {
+ if (datalen < SESSION_HEADER_SIZE || read_u32(blob + 8) !=
SESSION_VERSION) {
+ skipped_clear(blob, datalen);
+ free(blob);
+ free(session);
+ signal_error(env, OMEMO_ECORRUPT, "invalid session envelope");
+ return Qnil_v;
+ }
+ raw_size = read_u32(blob + 12);
+ key_count = read_u32(blob + 16);
+ if (key_count > SKIPPED_KEYS_MAX ||
+ raw_size > datalen - SESSION_HEADER_SIZE ||
+ key_count > (SIZE_MAX - SESSION_HEADER_SIZE - raw_size) /
+ SKIPPED_KEY_SIZE ||
+ SESSION_HEADER_SIZE + raw_size +
+ (size_t)key_count * SKIPPED_KEY_SIZE != datalen) {
+ skipped_clear(blob, datalen);
+ free(blob);
+ free(session);
+ signal_error(env, OMEMO_ECORRUPT, "malformed session envelope");
+ return Qnil_v;
+ }
+ raw = blob + SESSION_HEADER_SIZE;
+ }
+ int rc = omemoDeserializeSession(raw, raw_size, &session->session);
+ if (!rc && key_count) {
+ const uint8_t *p = raw + raw_size;
+ for (uint32_t i = 0; i < key_count; i++, p += SKIPPED_KEY_SIZE)
+ if (skipped_add(session, read_u32(p), p + 4, p + 36)) {
+ rc = OMEMO_ESTORE;
+ break;
+ }
+ }
+ skipped_clear(blob, datalen);
free(blob);
if (rc) {
+ skipped_drop(session);
free(session);
signal_error(env, rc, "omemoDeserializeSession failed");
return Qnil_v;
@@ -759,6 +878,34 @@ F_deserialize_session(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
return env->make_user_ptr(env, free_session, session);
}
+/* jabber-omemo--legacy-session-blob-p */
+
+static emacs_value
+F_legacy_session_blob_p(emacs_env *env, ptrdiff_t nargs, emacs_value *args,
+ void *data)
+{
+ (void)nargs; (void)data;
+
+ ptrdiff_t bloblen = 0;
+ env->copy_string_contents(env, args[0], NULL, &bloblen);
+ if (env->non_local_exit_check(env))
+ return Qnil_v;
+ uint8_t *blob = malloc((size_t)bloblen);
+ if (!blob) {
+ signal_error(env, -1, "malloc failed");
+ return Qnil_v;
+ }
+ if (!env->copy_string_contents(env, args[0], (char *)blob, &bloblen)) {
+ free(blob);
+ return Qnil_v;
+ }
+ emacs_value result = session_envelope_p(blob, (size_t)bloblen - 1)
+ ? Qnil_v : Qt_v;
+ skipped_clear(blob, (size_t)bloblen - 1);
+ free(blob);
+ return result;
+}
+
/* jabber-omemo--encrypt-key */
static emacs_value
@@ -767,7 +914,7 @@ F_encrypt_key(emacs_env *env, ptrdiff_t nargs, emacs_value
*args,
{
(void)nargs; (void)data;
- struct omemoSession *session = env->get_user_ptr(env, args[0]);
+ struct native_session *native = extract_session(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
@@ -780,7 +927,7 @@ F_encrypt_key(emacs_env *env, ptrdiff_t nargs, emacs_value
*args,
struct omemoKeyMessage msg;
memset(&msg, 0, sizeof(msg));
- int rc = omemoEncryptKey(session, &msg, keybuf, keylen);
+ int rc = omemoEncryptKey(&native->session, &msg, keybuf, keylen);
if (rc) {
signal_error(env, rc, "omemoEncryptKey failed");
return Qnil_v;
@@ -804,11 +951,11 @@ F_decrypt_key(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoSession *session = env->get_user_ptr(env, args[0]);
+ struct native_session *native = extract_session(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
- struct omemoStore *store = env->get_user_ptr(env, args[1]);
+ struct omemoStore *store = extract_store(env, args[1]);
if (env->non_local_exit_check(env))
return Qnil_v;
@@ -835,13 +982,33 @@ F_decrypt_key(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
uint8_t key[OMEMO_KEYSIZE];
size_t keyn = sizeof(key);
- int rc = omemoDecryptKey(session, store, key, &keyn,
+ size_t old_count = native->count;
+ struct skipped_key *old_keys = NULL;
+ if (old_count) {
+ old_keys = malloc(old_count * sizeof *old_keys);
+ if (!old_keys) {
+ free(msgbuf);
+ signal_error(env, OMEMO_ESTORE, "cannot snapshot skipped keys");
+ return Qnil_v;
+ }
+ memcpy(old_keys, native->keys, old_count * sizeof *old_keys);
+ }
+ native->decrypt_new_count = 0;
+ int rc = omemoDecryptKey(&native->session, store, key, &keyn,
isprekey, msgbuf, msglen);
+ native->decrypt_new_count = 0;
free(msgbuf);
if (rc) {
+ skipped_drop(native);
+ native->keys = old_keys;
+ native->count = native->cap = old_count;
signal_error(env, rc, "omemoDecryptKey failed");
return Qnil_v;
}
+ if (old_keys) {
+ skipped_clear(old_keys, old_count * sizeof *old_keys);
+ free(old_keys);
+ }
return make_unibyte(env, key, keyn);
}
@@ -854,20 +1021,19 @@ F_session_skipped_keys(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
{
(void)nargs; (void)data;
- struct omemoSession *session = env->get_user_ptr(env, args[0]);
+ struct native_session *session = extract_session(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
- struct session_skipped *e = skipped_find(session, 0);
- if (!e || !e->count)
+ if (!session->count)
return Qnil_v;
- size_t count = e->count;
+ size_t count = session->count;
struct skipped_key *snapshot = malloc(count * sizeof *snapshot);
if (!snapshot && count) {
signal_error(env, OMEMO_ESTORE, "cannot snapshot skipped keys");
return Qnil_v;
}
- memcpy(snapshot, e->keys, count * sizeof *snapshot);
+ memcpy(snapshot, session->keys, count * sizeof *snapshot);
emacs_value Qlist = env->intern(env, "list");
emacs_value Qcons = env->intern(env, "cons");
@@ -890,48 +1056,164 @@ F_session_skipped_keys(emacs_env *env, ptrdiff_t nargs,
emacs_value *args,
/* jabber-omemo--session-set-skipped-keys */
+struct skipped_key_symbols {
+ emacs_value car, cdr, consp, integerp, stringp;
+};
+
+static int
+intern_skipped_key_symbols(emacs_env *env, struct skipped_key_symbols *symbols)
+{
+ symbols->car = env->intern(env, "car");
+ if (env->non_local_exit_check(env))
+ return -1;
+ symbols->cdr = env->intern(env, "cdr");
+ if (env->non_local_exit_check(env))
+ return -1;
+ symbols->consp = env->intern(env, "consp");
+ if (env->non_local_exit_check(env))
+ return -1;
+ symbols->integerp = env->intern(env, "integerp");
+ if (env->non_local_exit_check(env))
+ return -1;
+ symbols->stringp = env->intern(env, "stringp");
+ return env->non_local_exit_check(env) ? -1 : 0;
+}
+
+static int
+emacs_truth(emacs_env *env, emacs_value value, bool *result)
+{
+ *result = env->is_not_nil(env, value);
+ return env->non_local_exit_check(env) ? -1 : 0;
+}
+
+static int
+take_cons(emacs_env *env, const struct skipped_key_symbols *symbols,
+ emacs_value *list, emacs_value *value, const char *message)
+{
+ emacs_value proper = env->funcall(env, symbols->consp, 1, list);
+ if (env->non_local_exit_check(env))
+ return -1;
+ bool is_cons;
+ if (emacs_truth(env, proper, &is_cons))
+ return -1;
+ if (!is_cons) {
+ signal_error(env, OMEMO_EPARAM, message);
+ return -1;
+ }
+ *value = env->funcall(env, symbols->car, 1, list);
+ if (env->non_local_exit_check(env))
+ return -1;
+ *list = env->funcall(env, symbols->cdr, 1, list);
+ return env->non_local_exit_check(env) ? -1 : 0;
+}
+
+static int
+require_type(emacs_env *env, emacs_value predicate, emacs_value value,
+ const char *message)
+{
+ emacs_value valid = env->funcall(env, predicate, 1, &value);
+ if (env->non_local_exit_check(env))
+ return -1;
+ bool matches;
+ if (emacs_truth(env, valid, &matches))
+ return -1;
+ if (!matches) {
+ signal_error(env, OMEMO_EPARAM, message);
+ return -1;
+ }
+ return 0;
+}
+
+static int
+parse_skipped_key(emacs_env *env,
+ const struct skipped_key_symbols *symbols,
+ emacs_value entry, struct skipped_key *key)
+{
+ emacs_value nr, dh, mk;
+ if (take_cons(env, symbols, &entry, &nr,
+ "malformed skipped key entry") ||
+ take_cons(env, symbols, &entry, &dh,
+ "malformed skipped key entry") ||
+ take_cons(env, symbols, &entry, &mk,
+ "malformed skipped key entry"))
+ return -1;
+ bool has_tail;
+ if (emacs_truth(env, entry, &has_tail))
+ return -1;
+ if (has_tail) {
+ signal_error(env, OMEMO_EPARAM, "malformed skipped key entry");
+ return -1;
+ }
+ if (require_type(env, symbols->integerp, nr,
+ "skipped key message number must be an integer") ||
+ require_type(env, symbols->stringp, dh,
+ "skipped dh key must be a string") ||
+ require_type(env, symbols->stringp, mk,
+ "skipped message key must be a string"))
+ return -1;
+
+ intmax_t number = env->extract_integer(env, nr);
+ if (env->non_local_exit_check(env))
+ return -1;
+ if (number < 0 || number > UINT32_MAX) {
+ signal_error(env, OMEMO_EPARAM,
+ "skipped key message number is out of range");
+ return -1;
+ }
+ uint8_t dh_buf[33], mk_buf[33];
+ if (extract_exact_unibyte(env, dh, dh_buf, 32,
+ "skipped dh key must be exactly 32 bytes") ||
+ extract_exact_unibyte(env, mk, mk_buf, 32,
+ "skipped message key must be exactly 32 bytes"))
+ return -1;
+ key->nr = (uint32_t)number;
+ memcpy(key->dh, dh_buf, sizeof key->dh);
+ memcpy(key->mk, mk_buf, sizeof key->mk);
+ return 0;
+}
+
static emacs_value
F_session_set_skipped_keys(emacs_env *env, ptrdiff_t nargs,
emacs_value *args, void *data)
{
(void)nargs; (void)data;
- struct omemoSession *session = env->get_user_ptr(env, args[0]);
+ struct native_session *session = extract_session(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
- emacs_value Qcar = env->intern(env, "car");
- emacs_value Qcdr = env->intern(env, "cdr");
-
- skipped_drop(session);
- for (emacs_value l = args[1]; env->is_not_nil(env, l);
- l = env->funcall(env, Qcdr, 1, &l)) {
- emacs_value entry = env->funcall(env, Qcar, 1, &l);
- emacs_value v_nr = env->funcall(env, Qcar, 1, &entry);
- emacs_value rest = env->funcall(env, Qcdr, 1, &entry);
- emacs_value v_dh = env->funcall(env, Qcar, 1, &rest);
- rest = env->funcall(env, Qcdr, 1, &rest);
- emacs_value v_mk = env->funcall(env, Qcar, 1, &rest);
-
- intmax_t nr = env->extract_integer(env, v_nr);
- uint8_t dh[33], mk[33];
- size_t dhn = 0, mkn = 0;
- if (extract_unibyte(env, v_dh, dh, sizeof(dh), &dhn) ||
- extract_unibyte(env, v_mk, mk, sizeof(mk), &mkn))
- return Qnil_v;
- if (env->non_local_exit_check(env))
- return Qnil_v;
- if (dhn != 32 || mkn != 32) {
- signal_error(env, OMEMO_EPARAM,
- "skipped key entry must hold 32-byte dh and mk");
- return Qnil_v;
- }
- struct session_skipped *e = skipped_find(session, 1);
- if (!e || skipped_add(e, (uint32_t)nr, dh, mk)) {
+ struct skipped_key_symbols symbols;
+ if (intern_skipped_key_symbols(env, &symbols))
+ return Qnil_v;
+ struct native_session temporary = {0};
+ emacs_value l = args[1];
+
+ for (;;) {
+ bool more;
+ if (emacs_truth(env, l, &more))
+ goto fail;
+ if (!more)
+ break;
+ emacs_value entry;
+ struct skipped_key key;
+ if (take_cons(env, &symbols, &l, &entry,
+ "skipped keys must be a proper list") ||
+ parse_skipped_key(env, &symbols, entry, &key))
+ goto fail;
+ if (skipped_add(&temporary, key.nr, key.dh, key.mk)) {
signal_error(env, OMEMO_ESTORE, "cannot store skipped key");
- return Qnil_v;
+ goto fail;
}
}
+
+ skipped_drop(session);
+ session->keys = temporary.keys;
+ session->count = temporary.count;
+ session->cap = temporary.cap;
+ return Qnil_v;
+
+fail:
+ skipped_drop(&temporary);
return Qnil_v;
}
@@ -943,18 +1225,18 @@ F_heartbeat(emacs_env *env, ptrdiff_t nargs, emacs_value
*args,
{
(void)nargs; (void)data;
- struct omemoSession *session = env->get_user_ptr(env, args[0]);
+ struct native_session *native = extract_session(env, args[0]);
if (env->non_local_exit_check(env))
return Qnil_v;
- struct omemoStore *store = env->get_user_ptr(env, args[1]);
+ struct omemoStore *store = extract_store(env, args[1]);
if (env->non_local_exit_check(env))
return Qnil_v;
struct omemoKeyMessage msg;
memset(&msg, 0, sizeof(msg));
- int rc = omemoHeartbeat(session, store, &msg);
+ int rc = omemoHeartbeat(&native->session, store, &msg);
if (rc) {
signal_error(env, rc, "omemoHeartbeat failed");
return Qnil_v;
@@ -1242,6 +1524,10 @@ emacs_module_init(struct emacs_runtime *runtime)
"Deserialize BLOB into an OMEMO session object.\n"
"Returns a session user-ptr; freed automatically by GC.");
+ DEFUN("jabber-omemo--legacy-session-blob-p",
+ F_legacy_session_blob_p, 1, 1,
+ "Return non-nil when BLOB uses the legacy raw session format.");
+
DEFUN("jabber-omemo--encrypt-key", F_encrypt_key, 2, 2,
"Encrypt KEY for a recipient using SESSION-PTR.\n"
"KEY is a unibyte string (the message encryption key).\n"
diff --git a/src/picomemo/omemo.c b/src/picomemo/omemo.c
index 44f63b5844..0e393a2759 100644
--- a/src/picomemo/omemo.c
+++ b/src/picomemo/omemo.c
@@ -170,8 +170,8 @@ struct ProtobufField {
/**
* Parse Protobuf varint.
*
- * Only supports uint32, higher bits are skipped so it will neither
- * overflow nor clamp to UINT32_MAX.
+ * Only supports uint32. Encodings wider than five bytes and values
+ * whose fifth byte exceeds the remaining four bits are rejected.
*
* @param s points to the location of the varint in the protobuf data
* @param e points to the end of the protobuf data
@@ -182,15 +182,18 @@ struct ProtobufField {
*/
static const uint8_t *ParseVarInt(const uint8_t *s, const uint8_t *e,
uint32_t *v) {
- int i = 0;
*v = 0;
- do {
+ for (unsigned int i = 0; i < 5; i++) {
if (s >= e)
return NULL;
- *v |= (*s & 0x7f) << i;
- i += 7;
- } while (*s++ & 0x80);
- return s;
+ uint8_t byte = *s++;
+ if (i == 4 && (byte & 0x7f) > 0x0f)
+ return NULL;
+ *v |= (uint32_t)(byte & 0x7f) << (7 * i);
+ if (!(byte & 0x80))
+ return s;
+ }
+ return NULL;
}
/**
@@ -238,6 +241,8 @@ static bool ParseProtobuf(const uint8_t *s, size_t n,
return true;
fields[id].v = v;
if (type == PB_LEN) {
+ if ((size_t)(e - s) < fields[id].v)
+ return true;
fields[id].p = s;
s += fields[id].v;
}
@@ -268,6 +273,8 @@ static bool ParseRepeatingField(const uint8_t *s, const
uint8_t *e,
if (id == fieldid)
field->v = v;
if (type == PB_LEN) {
+ if ((size_t)(e - s) < v)
+ return true;
if (id == fieldid)
field->p = s;
s += v;
diff --git a/tests/jabber-test-mam.el b/tests/jabber-test-mam.el
index 394d9a8d89..d58cca58e6 100644
--- a/tests/jabber-test-mam.el
+++ b/tests/jabber-test-mam.el
@@ -14,6 +14,7 @@
(require 'jabber-mam)
(require 'jabber-core)
(require 'jabber-message-correct)
+(require 'jabber-omemo-store)
;;; Test infrastructure
@@ -187,6 +188,39 @@ When COMPLETE is non-nil, mark the archive as fully
consumed."
;; Batched should be under 2 seconds for 500 messages
(should (< batched-time 2.0)))))))
+(ert-deftest jabber-test-mam-encrypted-session-save-in-transaction ()
+ "An encrypted MAM message can migrate its session inside the MAM
transaction."
+ (jabber-test-mam-with-db
+ (let* ((jc (jabber-test-mam--make-fake-jc "[email protected]"))
+ (jabber-mam--syncing (list (cons jc jabber-test-mam-queryid)))
+ (jabber-muc-participants nil)
+ (stanza (jabber-test-mam--make-message 1))
+ (inner (nth 2 (jabber-mam--parse-result stanza)))
+ (envelope (unibyte-string ?J ?O ?M ?E ?M ?O 0 1 0))
+ (body (car (jabber-xml-get-children inner 'body))))
+ (setcdr (last inner) '((encrypted ((xmlns .
"eu.siacs.conversations.axolotl")))))
+ (sqlite-execute jabber-db--connection "\
+INSERT INTO omemo_skipped_keys
+ (account, jid, device_id, dh_key, message_number, message_key, created_at)
+ VALUES ('[email protected]', '[email protected]', 7, 'dh', 3, 'mk', 0)")
+ (sqlite-execute jabber-db--connection "BEGIN")
+ (cl-letf (((symbol-function 'jabber-chat--decrypt-if-needed)
+ (lambda (_jc message)
+ (jabber-omemo-store-save-session-and-clear-legacy-keys
+ "[email protected]" "[email protected]" 7 envelope)
+ (setcdr (cdr body) '("decrypted"))
+ message)))
+ (jabber-mam--process-message jc stanza))
+ (sqlite-execute jabber-db--connection "COMMIT")
+ (should (equal envelope
+ (jabber-omemo-store-load-session
+ "[email protected]" "[email protected]" 7)))
+ (should-not (jabber-omemo-store-all-skipped-keys
+ "[email protected]" "[email protected]" 7))
+ (should (equal "decrypted"
+ (caar (sqlite-select jabber-db--connection "\
+SELECT body FROM message WHERE stanza_id = 'stanza-000001'")))))))
+
;;; Group 4: Parse helpers
(ert-deftest jabber-test-mam-parse-result ()
diff --git a/tests/jabber-test-omemo-message.el
b/tests/jabber-test-omemo-message.el
index 62199f3ec0..f5354f958c 100644
--- a/tests/jabber-test-omemo-message.el
+++ b/tests/jabber-test-omemo-message.el
@@ -510,8 +510,7 @@ buffer-local `jabber-group'."
(jabber-omemo--sessions (make-hash-table :test 'equal)))
(puthash "[email protected]" 42 jabber-omemo--device-ids)
(puthash "[email protected]" 'fake-store-ptr jabber-omemo--stores)
- ;; A real session pointer: the skipped-key preload hands it to the
- ;; C module, which rejects placeholder symbols.
+ ;; Use a real session pointer because the C module rejects placeholders.
(puthash (jabber-omemo--session-key "[email protected]" "[email protected]"
999)
(jabber-omemo-make-session) jabber-omemo--sessions)
(cl-letf (((symbol-function 'jabber-connection-bare-jid)
diff --git a/tests/jabber-test-omemo-module.el
b/tests/jabber-test-omemo-module.el
index d8a9d088e0..3e45b39da3 100644
--- a/tests/jabber-test-omemo-module.el
+++ b/tests/jabber-test-omemo-module.el
@@ -63,6 +63,35 @@
(blob2 (jabber-omemo--serialize-store ptr)))
(should (string= blob1 blob2))))
+(ert-deftest jabber-test-omemo-module-protobuf-rejects-bad-varints ()
+ "Deserialization rejects oversized and unterminated uint32 varints."
+ (dolist (blob (list (unibyte-string #x08 #x80 #x80 #x80 #x80 #x80 #x00)
+ (unibyte-string #x08 #x80 #x80 #x80 #x80 #x80)
+ (unibyte-string #x08 #x80 #x80 #x80 #x80 #x10)))
+ (should-error (jabber-omemo--deserialize-store blob)
+ :type 'jabber-omemo-error)))
+
+(ert-deftest jabber-test-omemo-module-protobuf-rejects-truncated-field ()
+ "Deserialization rejects a length field beyond the remaining input."
+ (should-error
+ (jabber-omemo--deserialize-store (unibyte-string #x12 #x20))
+ :type 'jabber-omemo-error))
+
+(ert-deftest jabber-test-omemo-module-pointer-kinds-are-distinct ()
+ "Store and session wrappers reject the other native pointer kind."
+ (let ((store (jabber-omemo--deserialize-store (jabber-omemo--setup-store)))
+ (session (jabber-omemo--make-session)))
+ (dolist (form (list (lambda () (jabber-omemo--serialize-store session))
+ (lambda () (jabber-omemo--get-bundle session))
+ (lambda () (jabber-omemo--serialize-session store))
+ (lambda () (jabber-omemo--used-pre-key-id store))
+ (lambda () (jabber-omemo--encrypt-key store
+ (make-string 32
0)))
+ (lambda () (jabber-omemo--decrypt-key
+ session session nil "bad"))
+ (lambda () (jabber-omemo--heartbeat session session))))
+ (should-error (funcall form) :type 'jabber-omemo-error))))
+
;;; Group 3: Bundle extraction
(ert-deftest jabber-test-omemo-module-get-bundle-plist-keys ()
@@ -184,6 +213,19 @@
(plist-get enc :ciphertext))))
(should (string= msg dec))))
+(ert-deftest jabber-test-omemo-module-decrypt-requires-exact-key-and-iv ()
+ "Message decryption rejects short and oversized keys and IVs."
+ (let* ((enc (jabber-omemo--encrypt-message "payload"))
+ (key (plist-get enc :key))
+ (iv (plist-get enc :iv))
+ (ciphertext (plist-get enc :ciphertext)))
+ (dolist (bad-key (list (substring key 1) (concat key "x")))
+ (should-error (jabber-omemo--decrypt-message bad-key iv ciphertext)
+ :type 'jabber-omemo-error))
+ (dolist (bad-iv (list (substring iv 1) (concat iv "x")))
+ (should-error (jabber-omemo--decrypt-message key bad-iv ciphertext)
+ :type 'jabber-omemo-error))))
+
(ert-deftest jabber-test-omemo-module-decrypt-wrong-key-signals-error ()
"Decrypting with a wrong key signals jabber-omemo-error."
(let* ((msg (encode-coding-string "secret" 'utf-8))
@@ -259,6 +301,49 @@
(should (user-ptrp session2))
(should (string= blob1 blob2))))
+(defun jabber-test-omemo-module--set-u32 (string offset value)
+ "Store big-endian VALUE in STRING at OFFSET."
+ (dotimes (i 4)
+ (aset string (+ offset i) (logand 255 (ash value (* -8 (- 3 i))))))
+ string)
+
+(ert-deftest jabber-test-omemo-module-session-envelope-validation ()
+ "Malformed envelopes, excess keys, and trailing data are rejected."
+ (let* ((session (jabber-omemo--make-session))
+ (blob (jabber-omemo--serialize-session session))
+ (bad-version (copy-sequence blob))
+ (too-many (copy-sequence blob)))
+ (aset bad-version 11 2)
+ (jabber-test-omemo-module--set-u32 too-many 16 1001)
+ (dolist (bad (list bad-version too-many (concat blob "x")))
+ (should-error (jabber-omemo--deserialize-session bad)
+ :type 'jabber-omemo-error))))
+
+(ert-deftest jabber-test-omemo-module-session-legacy-blob-loads ()
+ "The raw picomemo payload remains a valid legacy session blob."
+ (let* ((session (jabber-omemo--make-session))
+ (blob (jabber-omemo--serialize-session session))
+ (raw-size (+ (ash (aref blob 12) 24)
+ (ash (aref blob 13) 16)
+ (ash (aref blob 14) 8)
+ (aref blob 15)))
+ (raw (substring blob 20 (+ 20 raw-size))))
+ (should-not (jabber-omemo--legacy-session-blob-p blob))
+ (should (jabber-omemo--legacy-session-blob-p raw))
+ (should (user-ptrp (jabber-omemo--deserialize-session raw)))))
+
+(ert-deftest jabber-test-omemo-module-skipped-key-fifo-limit ()
+ "A session retains the newest 1000 imported skipped keys in FIFO order."
+ (let* ((session (jabber-omemo--make-session))
+ (dh (make-string 32 ?d))
+ (keys (cl-loop for i below 1001
+ collect (list i dh (make-string 32 ?m)))))
+ (jabber-omemo--session-set-skipped-keys session keys)
+ (let ((stored (jabber-omemo--session-skipped-keys session)))
+ (should (= 1000 (length stored)))
+ (should (= 1 (caar stored)))
+ (should (= 1000 (car (car (last stored))))))))
+
(ert-deftest jabber-test-omemo-module-initiate-session-bad-signature ()
"initiate-session with a bad signature signals an error."
(let* ((alice-blob (jabber-omemo--setup-store))
@@ -278,6 +363,28 @@
(car pk))
:type 'jabber-omemo-error)))
+(ert-deftest jabber-test-omemo-module-initiate-requires-exact-inputs ()
+ "Session initiation rejects short and oversized cryptographic inputs."
+ (let* ((alice (jabber-omemo--deserialize-store (jabber-omemo--setup-store)))
+ (bob (jabber-omemo--deserialize-store (jabber-omemo--setup-store)))
+ (bundle (jabber-omemo--get-bundle bob))
+ (signature (plist-get bundle :signature))
+ (spk (plist-get bundle :signed-pre-key))
+ (ik (plist-get bundle :identity-key))
+ (pk (car (plist-get bundle :pre-keys)))
+ (inputs (list signature spk ik (cdr pk))))
+ (dotimes (index 4)
+ (dolist (bad (list (substring (nth index inputs) 1)
+ (concat (nth index inputs) "x")))
+ (let ((changed (copy-sequence inputs)))
+ (setf (nth index changed) bad)
+ (should-error
+ (apply #'jabber-omemo--initiate-session
+ alice (append changed
+ (list (plist-get bundle :signed-pre-key-id)
+ (car pk))))
+ :type 'jabber-omemo-error))))))
+
;;; Group 7: Key encrypt/decrypt round-trip
(ert-deftest jabber-test-omemo-module-encrypt-key-returns-plist ()
@@ -349,6 +456,19 @@
bob-session bob is-prekey enc-data)))
(should (string= original-key decrypted))))
+(ert-deftest jabber-test-omemo-module-decrypt-key-rejects-bad-protobuf ()
+ "Key decryption rejects hostile varints and truncated length fields."
+ (let ((store (jabber-omemo--deserialize-store (jabber-omemo--setup-store)))
+ (session (jabber-omemo--make-session)))
+ (dolist (message
+ (list (unibyte-string #x33 #x08 #x80 #x80 #x80 #x80 #x80 #x00)
+ (unibyte-string #x33 #x08 #x80 #x80 #x80 #x80 #x80)
+ (unibyte-string #x33 #x08 #x80 #x80 #x80 #x80 #x10)
+ (unibyte-string #x33 #x12 #x20)))
+ (should-error
+ (jabber-omemo--decrypt-key session store t message)
+ :type 'jabber-omemo-error))))
+
(ert-deftest jabber-test-omemo-module-consecutive-messages-are-pre-key ()
"Consecutive messages from initiator stay pre-key until reply."
(let* ((alice-blob (jabber-omemo--setup-store))
@@ -537,6 +657,33 @@ Alice has initiated a session towards Bob's bundle."
(jabber-omemo--session-set-skipped-keys session nil)
(should (null (jabber-omemo--session-skipped-keys session)))))
+(ert-deftest jabber-test-omemo-module-skipped-key-import-is-atomic ()
+ "Rejected skipped-key imports preserve the session's original keys."
+ (let* ((session (jabber-omemo--make-session))
+ (dh (make-string 32 ?d))
+ (mk (make-string 32 ?m))
+ (original (list (list 7 dh mk)))
+ (bad-lists
+ (list (cons (list 8 dh mk) 9)
+ (list 8 dh)
+ (list 8 dh mk 'extra)
+ (list (cons 8 (cons dh mk)))
+ (list (list 'bad dh mk))
+ (list (list -1 dh mk))
+ (list (list (1+ #xffffffff) dh mk))
+ (list (list 8 'bad mk))
+ (list (list 8 dh 'bad))
+ (list (list 8 (make-string 31 ?d) mk))
+ (list (list 8 (make-string 33 ?d) mk))
+ (list (list 8 dh (make-string 31 ?m)))
+ (list (list 8 dh (make-string 33 ?m))))))
+ (jabber-omemo--session-set-skipped-keys session original)
+ (dolist (bad bad-lists)
+ (should-error (jabber-omemo--session-set-skipped-keys session bad)
+ :type 'jabber-omemo-error)
+ (should (equal original
+ (jabber-omemo--session-skipped-keys session))))))
+
(ert-deftest jabber-test-omemo-module-skipped-keys-survive-finalizers ()
"Enumerating skipped keys is safe while other sessions are finalized."
(let* ((session (jabber-omemo--make-session))
@@ -619,14 +766,11 @@ Alice has initiated a session towards Bob's bundle."
(should (string= k2 (jabber-omemo--decrypt-key
bob-session bob
(plist-get m2 :pre-key-p) (plist-get m2 :data))))
- ;; Simulate a restart: serialize the session and its skipped
- ;; keys, then restore both into a fresh session pointer.
- (let* ((skipped (jabber-omemo--session-skipped-keys bob-session))
- (blob (jabber-omemo--serialize-session bob-session))
+ ;; Simulate a restart. The session blob owns its skipped keys.
+ (let* ((blob (jabber-omemo--serialize-session bob-session))
(restored (jabber-omemo--deserialize-session blob)))
- (should (= 1 (length skipped)))
- (should (null (jabber-omemo--session-skipped-keys restored)))
- (jabber-omemo--session-set-skipped-keys restored skipped)
+ (should (= 1 (length
+ (jabber-omemo--session-skipped-keys restored))))
(should (string= k1 (jabber-omemo--decrypt-key
restored bob
(plist-get m1 :pre-key-p)
diff --git a/tests/jabber-test-omemo-protocol.el
b/tests/jabber-test-omemo-protocol.el
index e12d295c96..321d90ad15 100644
--- a/tests/jabber-test-omemo-protocol.el
+++ b/tests/jabber-test-omemo-protocol.el
@@ -670,19 +670,46 @@ skipped message keys, so no fresh-session fallback is
needed."
;;; Group: Skipped message keys
-(ert-deftest jabber-test-omemo-protocol-skipped-key-changes-diff ()
- "Diff returns new keys and consumed keys by NR + DH identity."
- (let* ((a (list 1 (make-string 32 ?a) (make-string 32 ?x)))
- (b (list 2 (make-string 32 ?b) (make-string 32 ?y)))
- (c (list 3 (make-string 32 ?c) (make-string 32 ?z))))
- (pcase-let ((`(,new . ,consumed)
- (jabber-omemo--skipped-key-changes (list a b) (list b c))))
- (should (equal (list c) new))
- (should (equal (list a) consumed)))))
+(ert-deftest jabber-test-omemo-protocol-legacy-session-migrates-atomically ()
+ "A raw session imports legacy keys and saves one self-contained blob."
+ (jabber-test-omemo-protocol-with-db
+ (cl-letf (((symbol-function 'jabber-connection-bare-jid)
+ (lambda (_jc) "[email protected]")))
+ (let* ((jc 'jc)
+ (session (jabber-omemo-make-session))
+ (envelope (jabber-omemo-serialize-session session))
+ (raw-size (+ (ash (aref envelope 12) 24)
+ (ash (aref envelope 13) 16)
+ (ash (aref envelope 14) 8)
+ (aref envelope 15)))
+ (raw (substring envelope 20 (+ 20 raw-size)))
+ (dh (make-string 32 ?d))
+ (mk (make-string 32 ?m)))
+ (jabber-omemo-store-save-session
+ "[email protected]" "[email protected]" 111 raw)
+ (sqlite-execute jabber-db--connection "\
+INSERT INTO omemo_skipped_keys
+ (account, jid, device_id, dh_key, message_number, message_key, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)"
+ (list "[email protected]" "[email protected]"
+ 111 dh 7 mk 0))
+ (let ((loaded (jabber-omemo--get-session
+ jc "[email protected]" 111)))
+ (should (equal (list (list 7 dh mk))
+ (jabber-omemo--session-skipped-keys loaded)))
+ (jabber-omemo--save-session jc "[email protected]" 111 loaded))
+ (let ((blob (jabber-omemo-store-load-session
+ "[email protected]" "[email protected]" 111)))
+ (should-not (jabber-omemo--legacy-session-blob-p blob))
+ (should (= 1 (length
+ (jabber-omemo--session-skipped-keys
+ (jabber-omemo-deserialize-session blob))))))
+ (should-not (jabber-omemo-store-all-skipped-keys
+ "[email protected]" "[email protected]" 111))))))
(ert-deftest jabber-test-omemo-protocol-skipped-keys-recover-across-restart ()
"An out-of-order message decrypts after a session cache flush.
-Skipped ratchet keys persist in SQLite and reload with the session."
+Skipped ratchet keys persist inside the session blob."
(jabber-test-omemo-protocol-with-db
(cl-letf (((symbol-function 'jabber-connection-bare-jid)
(lambda (_jc) "[email protected]")))
@@ -711,8 +738,8 @@ Skipped ratchet keys persist in SQLite and reload with the
session."
(should (string= k2 key))
(should fresh-p)
(jabber-omemo--save-session jc "[email protected]" 111 session))
- (should (= 1 (length (jabber-omemo-store-all-skipped-keys
- "[email protected]" "[email protected]" 111))))
+ (should-not (jabber-omemo-store-all-skipped-keys
+ "[email protected]" "[email protected]" 111))
;; Simulate a restart: drop all in-memory session state.
(clrhash jabber-omemo--sessions)
;; The late message decrypts from the reloaded skipped key.
@@ -721,7 +748,7 @@ Skipped ratchet keys persist in SQLite and reload with the
session."
jc "[email protected]" 111 store-ptr
(plist-get m1 :pre-key-p) (plist-get m1 :data))))
(should (string= k1 key)))
- ;; Consumed: the persisted copy is gone.
+ ;; No separate skipped-key rows are written or consumed.
(should (null (jabber-omemo-store-all-skipped-keys
"[email protected]" "[email protected]" 111)))))))
diff --git a/tests/jabber-test-omemo-store.el b/tests/jabber-test-omemo-store.el
index dc6140e67a..def664a5c6 100644
--- a/tests/jabber-test-omemo-store.el
+++ b/tests/jabber-test-omemo-store.el
@@ -276,59 +276,77 @@ Binds `jabber-db-path' to a temp file and tears down on
exit."
(should (cl-find 42 all :key (lambda (p) (plist-get p :device-id))))
(should (cl-find 99 all :key (lambda (p) (plist-get p
:device-id))))))))
-;;; Group 6: Skipped key CRUD
+;;; Group 6: Legacy skipped-key migration
-(ert-deftest jabber-test-omemo-store-skipped-key-save-load-roundtrip ()
- "save + load round-trips a skipped key."
+(ert-deftest jabber-test-omemo-store-session-save-clears-legacy-keys ()
+ "Saving a self-contained session removes its legacy key rows."
(jabber-test-omemo-store-with-db
(let ((dh (encode-coding-string "dh-key-data" 'raw-text))
(mk (encode-coding-string "msg-key-data" 'raw-text)))
- (jabber-omemo-store-save-skipped-key "[email protected]"
"[email protected]"
- 42 dh 7 mk)
- (should (equal mk (jabber-omemo-store-load-skipped-key
- "[email protected]" "[email protected]" 42 dh 7))))))
-
-(ert-deftest jabber-test-omemo-store-skipped-key-load-unknown ()
- "load returns nil for unknown skipped key."
+ (sqlite-execute jabber-db--connection "\
+INSERT INTO omemo_skipped_keys
+ (account, jid, device_id, dh_key, message_number, message_key, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?)"
+ (list "[email protected]" "[email protected]"
+ 42 dh 7 mk 0))
+ (jabber-omemo-store-save-session-and-clear-legacy-keys
+ "[email protected]" "[email protected]" 42 "session")
+ (should (equal "session" (jabber-omemo-store-load-session
+ "[email protected]" "[email protected]" 42)))
+ (should-not (jabber-omemo-store-all-skipped-keys
+ "[email protected]" "[email protected]" 42)))))
+
+(ert-deftest jabber-test-omemo-store-session-save-composes-with-transaction ()
+ "Session migration succeeds inside an existing transaction."
(jabber-test-omemo-store-with-db
- (should (null (jabber-omemo-store-load-skipped-key
- "[email protected]" "[email protected]" 42
- (encode-coding-string "x" 'raw-text) 0)))))
-
-(ert-deftest jabber-test-omemo-store-skipped-key-delete ()
- "delete removes a skipped key after use."
+ (sqlite-execute jabber-db--connection "\
+INSERT INTO omemo_skipped_keys
+ (account, jid, device_id, dh_key, message_number, message_key, created_at)
+ VALUES ('[email protected]', '[email protected]', 42, 'dh', 7, 'mk', 0)")
+ (sqlite-execute jabber-db--connection "BEGIN")
+ (jabber-omemo-store-save-session-and-clear-legacy-keys
+ "[email protected]" "[email protected]" 42 "session")
+ (sqlite-execute jabber-db--connection "COMMIT")
+ (should (equal "session" (jabber-omemo-store-load-session
+ "[email protected]" "[email protected]" 42)))
+ (should-not (jabber-omemo-store-all-skipped-keys
+ "[email protected]" "[email protected]" 42))))
+
+(ert-deftest jabber-test-omemo-store-session-save-rolls-back-on-delete-error ()
+ "A failed legacy-key deletion restores the old session and key rows."
(jabber-test-omemo-store-with-db
- (let ((dh (encode-coding-string "dh" 'raw-text))
- (mk (encode-coding-string "mk" 'raw-text)))
- (jabber-omemo-store-save-skipped-key "[email protected]"
"[email protected]"
- 42 dh 7 mk)
- (jabber-omemo-store-delete-skipped-key "[email protected]"
"[email protected]"
- 42 dh 7)
- (should (null (jabber-omemo-store-load-skipped-key
- "[email protected]" "[email protected]" 42 dh 7))))))
-
-(ert-deftest jabber-test-omemo-store-skipped-key-delete-old ()
- "delete-old-skipped-keys removes by age."
+ (jabber-omemo-store-save-session
+ "[email protected]" "[email protected]" 42 "old-session")
+ (sqlite-execute jabber-db--connection "\
+INSERT INTO omemo_skipped_keys
+ (account, jid, device_id, dh_key, message_number, message_key, created_at)
+ VALUES ('[email protected]', '[email protected]', 42, 'dh', 7, 'mk', 0)")
+ (cl-letf (((symbol-function 'jabber-omemo-store-delete-skipped-keys)
+ (lambda (&rest _) (error "injected deletion failure"))))
+ (should-error
+ (jabber-omemo-store-save-session-and-clear-legacy-keys
+ "[email protected]" "[email protected]" 42 "new-session")
+ :type 'error))
+ (should (equal "old-session" (jabber-omemo-store-load-session
+ "[email protected]" "[email protected]" 42)))
+ (should (= 1 (length (jabber-omemo-store-all-skipped-keys
+ "[email protected]" "[email protected]" 42))))))
+
+(ert-deftest jabber-test-omemo-store-legacy-keys-load-fifo ()
+ "Legacy keys load chronologically with insertion order as a tie-breaker."
(jabber-test-omemo-store-with-db
- (let ((dh (encode-coding-string "dh" 'raw-text))
- (mk (encode-coding-string "mk" 'raw-text))
- (now (truncate (float-time))))
- ;; Insert an old key by directly using SQL
+ (dolist (row '((30 "dh-3" "mk-3" 20)
+ (10 "dh-1" "mk-1" 10)
+ (20 "dh-2" "mk-2" 10)))
(sqlite-execute jabber-db--connection "\
INSERT INTO omemo_skipped_keys
- (account, jid, device_id, dh_key, message_number, message_key, created_at)
- VALUES (?, ?, ?, ?, ?, ?, ?)"
- (list "[email protected]" "[email protected]" 42 dh 1 mk (- now 7200)))
- ;; Insert a recent key
- (jabber-omemo-store-save-skipped-key "[email protected]"
"[email protected]"
- 42 dh 2 mk)
- ;; Delete keys older than 1 hour
- (jabber-omemo-store-delete-old-skipped-keys "[email protected]" 3600)
- ;; Old key gone, recent key remains
- (should (null (jabber-omemo-store-load-skipped-key
- "[email protected]" "[email protected]" 42 dh 1)))
- (should (jabber-omemo-store-load-skipped-key
- "[email protected]" "[email protected]" 42 dh 2)))))
+ (account, jid, device_id, message_number, dh_key, message_key, created_at)
+ VALUES ('[email protected]', '[email protected]', 42, ?, ?, ?, ?)"
+ row))
+ (should (equal '(10 20 30)
+ (mapcar #'car
+ (jabber-omemo-store-all-skipped-keys
+ "[email protected]" "[email protected]" 42))))))
(provide 'jabber-test-omemo-store)