Commit immutable attachment identity before message publication
This commit is contained in:
1 parent
6803369a13
commit
fe75a493ef
2 files changed
+118
-14
No files matched your search
+55
-12
@@ -78,10 +78,11 @@ static void ak_hex(const uint8_t id[16], char hex[33]) {
|
||||
|
||||
/* Строгое имя UUID.uenc из подписанного manifest, без разделителей пути. */
|
||||
int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]) {
|
||||
uint8_t name[64];
|
||||
uint8_t name[64], decoded[16]; char canonical[52];
|
||||
const char* pipe = body ? memchr(body, '|', length) : NULL;
|
||||
if (!pipe || body[0] != '*' || pipe - body != 53 || b64_decode(body + 1, 52, name, sizeof(name)) != 37 ||
|
||||
memcmp(name + 32, ".uenc", 5)) goto invalid;
|
||||
if (!id || !pipe || body[0] != '*' || pipe - body != 53 || b64_decode(body + 1, 52, name, sizeof(name)) != 37 ||
|
||||
memcmp(name + 32, ".uenc", 5) || b64_encode(name, 37, canonical, sizeof(canonical)) != 52 ||
|
||||
memcmp(body + 1, canonical, sizeof(canonical))) goto invalid;
|
||||
for (unsigned i = 0; i < 16; i++) {
|
||||
unsigned value = 0;
|
||||
for (unsigned j = 0; j < 2; j++) {
|
||||
@@ -89,8 +90,9 @@ int chat_attachment_identity(const char* body, size_t length, uint8_t id[16]) {
|
||||
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) goto invalid;
|
||||
value = value * 16 + (c <= '9' ? c - '0' : c - 'a' + 10);
|
||||
}
|
||||
id[i] = (uint8_t)value;
|
||||
decoded[i] = (uint8_t)value;
|
||||
}
|
||||
memcpy(id, decoded, sizeof(decoded));
|
||||
return 0;
|
||||
invalid:
|
||||
DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid encrypted manifest identity bytes=%zu", length); return -1;
|
||||
@@ -372,16 +374,57 @@ fail:
|
||||
/* Сохраняется идентичность подписанного контейнера до выхода сообщения в сеть. */
|
||||
int chat_attachment_bind(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t id[16],
|
||||
const uint8_t message[32], const uint8_t hash[32]) {
|
||||
sqlite3_stmt* st = ak_prepare(inst, "UPDATE attachment_keys SET message_id=?,cipher_hash=?"
|
||||
" WHERE channel=? AND crypto_id=? AND author=? AND key IS NOT NULL AND (message_id IS NULL OR message_id=?)");
|
||||
if (!st) return -1;
|
||||
sqlite3_bind_blob(st, 1, message, 32, SQLITE_STATIC); sqlite3_bind_blob(st, 2, hash, 32, SQLITE_STATIC);
|
||||
sqlite3_bind_text(st, 3, channel, -1, SQLITE_STATIC); sqlite3_bind_blob(st, 4, id, 16, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(st, 5, (sqlite3_int64)inst->node_id); sqlite3_bind_blob(st, 6, message, 32, SQLITE_STATIC);
|
||||
if (ak_step(inst, st) || sqlite3_changes(CC(inst)->db) != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: author key missing or already bound channel=%s", channel); return -1;
|
||||
if (!inst || !CC(inst) || !CC(inst)->attachment_initialized || CC(inst)->attachment_closing ||
|
||||
!ak_channel(channel) || !id || !message || !hash) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: invalid author binding state/arguments"); return -1;
|
||||
}
|
||||
sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL;
|
||||
if (!sqlite3_get_autocommit(db)) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: binding rejected inside external transaction channel=%s", channel); return -1;
|
||||
}
|
||||
const char* stage = "begin binding";
|
||||
int rc = sqlite3_exec(db, "BEGIN IMMEDIATE /* attachment binding */", NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "prepare binding";
|
||||
rc = sqlite3_prepare_v2(db, "UPDATE attachment_keys SET message_id=?,cipher_hash=?"
|
||||
" WHERE channel=? AND crypto_id=? AND author=? AND length(key)=32"
|
||||
" AND ((message_id IS NULL AND cipher_hash IS NULL) OR (message_id=? AND cipher_hash=?))", -1, &st, NULL);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind message"; rc = sqlite3_bind_blob(st, 1, message, 32, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind hash"; rc = sqlite3_bind_blob(st, 2, hash, 32, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind channel"; rc = sqlite3_bind_text(st, 3, channel, -1, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind identity"; rc = sqlite3_bind_blob(st, 4, id, 16, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind author"; rc = sqlite3_bind_int64(st, 5, (sqlite3_int64)inst->node_id);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind previous message"; rc = sqlite3_bind_blob(st, 6, message, 32, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "bind previous hash"; rc = sqlite3_bind_blob(st, 7, hash, 32, SQLITE_STATIC);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
stage = "write binding"; rc = sqlite3_step(st);
|
||||
if (rc != SQLITE_DONE) goto fail;
|
||||
int changed = sqlite3_changes(db);
|
||||
stage = "finalize binding"; rc = sqlite3_finalize(st); st = NULL;
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
if (changed != 1) {
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: binding rows=%d expected=1 channel=%s", changed, channel);
|
||||
stage = "row count"; rc = SQLITE_CONSTRAINT; goto fail;
|
||||
}
|
||||
stage = "commit binding"; rc = sqlite3_exec(db, "COMMIT /* attachment binding */", NULL, NULL, NULL);
|
||||
if (rc != SQLITE_OK) goto fail;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "attachment keys: author binding committed channel=%s message=%02x%02x%02x%02x",
|
||||
channel, message[0], message[1], message[2], message[3]);
|
||||
return 0;
|
||||
fail:
|
||||
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: binding channel=%s stage=%s rc=%d (%s) db=%s",
|
||||
channel, stage, rc, sqlite3_errstr(rc), sqlite3_errmsg(db));
|
||||
int final_rc = sqlite3_finalize(st);
|
||||
if (final_rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: binding cleanup finalize rc=%d", final_rc);
|
||||
ak_rollback(inst, "author binding", channel);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Запись поступает только из проверенной истории; размер и hash относятся к ciphertext. */
|
||||
|
||||
@@ -68,8 +68,10 @@ int __wrap_sqlite3_exec(sqlite3* db, const char* sql, int (*cb)(void*, int, char
|
||||
if (fail_channel_rollback && !strcmp(sql, "ROLLBACK TO delete_topo_channel; RELEASE delete_topo_channel")) {
|
||||
fail_channel_rollback = 0; DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "delete test: injected rollback failure"); return SQLITE_IOERR;
|
||||
}
|
||||
int completion_scope = fault_prefix && strcmp(fault_prefix, "UPDATE attachment_keys SET decrypting=1 WHERE");
|
||||
int targeted = !completion_scope || strstr(sql, "/* attachment completion */");
|
||||
int binding_scope = fault_prefix && !strcmp(fault_prefix, "UPDATE attachment_keys SET message_id=");
|
||||
int completion_scope = fault_prefix && !binding_scope && strcmp(fault_prefix, "UPDATE attachment_keys SET decrypting=1 WHERE");
|
||||
int targeted = binding_scope ? strstr(sql, "/* attachment binding */") != NULL :
|
||||
!completion_scope || strstr(sql, "/* attachment completion */");
|
||||
if (targeted && !strncmp(sql, "BEGIN IMMEDIATE", strlen("BEGIN IMMEDIATE")) && take(BEGIN)) return SQLITE_BUSY;
|
||||
if (targeted && !strncmp(sql, "COMMIT", strlen("COMMIT")) && take(COMMIT)) return SQLITE_BUSY;
|
||||
if (!strcmp(sql, "ROLLBACK") && fail_rollback && (!completion_scope || completion_transaction)) {
|
||||
@@ -607,6 +609,64 @@ static void test_channel_delete_rollback(void) {
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS channel delete:14 SQL faults, cascade/Merkle ABORT/IGNORE, veto/foreign transaction/rollback fault; key/files preserved until committed event");
|
||||
}
|
||||
|
||||
static void binding_state(struct fixture* f, const uint8_t message[32], const uint8_t hash[32], int bound) {
|
||||
sqlite3_stmt* st = NULL;
|
||||
assert(sqlite3_prepare_v2(f->reader, "SELECT message_id,cipher_hash FROM attachment_keys WHERE channel=? AND crypto_id=?",
|
||||
-1, &st, NULL) == SQLITE_OK);
|
||||
assert(sqlite3_bind_text(st, 1, f->channel, -1, SQLITE_STATIC) == SQLITE_OK);
|
||||
assert(sqlite3_bind_blob(st, 2, f->id, 16, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
|
||||
assert(sqlite3_column_bytes(st, 0) == 32 * bound && sqlite3_column_bytes(st, 1) == 32 * bound);
|
||||
if (bound) assert(!memcmp(sqlite3_column_blob(st, 0), message, 32) && !memcmp(sqlite3_column_blob(st, 1), hash, 32));
|
||||
assert(sqlite3_step(st) == SQLITE_DONE && sqlite3_finalize(st) == SQLITE_OK);
|
||||
}
|
||||
|
||||
static void test_attachment_binding(void) {
|
||||
struct fixture f; setup(&f); source(&f); publish(&f);
|
||||
uint8_t message[32], hash[32]; sqlite3_stmt* st = NULL;
|
||||
assert(sqlite3_prepare_v2(f.reader, "SELECT message_id,cipher_hash FROM attachment_keys WHERE channel=?", -1, &st, NULL) == SQLITE_OK);
|
||||
assert(sqlite3_bind_text(st, 1, f.channel, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW);
|
||||
assert(sqlite3_column_bytes(st, 0) == 32 && sqlite3_column_bytes(st, 1) == 32);
|
||||
memcpy(message, sqlite3_column_blob(st, 0), 32); memcpy(hash, sqlite3_column_blob(st, 1), 32); assert(sqlite3_finalize(st) == SQLITE_OK);
|
||||
char query[256]; snprintf(query, sizeof(query), "UPDATE attachment_keys SET message_id=NULL,cipher_hash=NULL WHERE channel='%s'", f.channel);
|
||||
const enum fault points[] = {BIND_1, BEGIN, PREPARE, BIND_2, BIND_3, BIND_4, BIND_5, BIND_6, BIND_7, STEP, FINALIZE, CHANGES, COMMIT};
|
||||
fault_prefix = "UPDATE attachment_keys SET message_id=";
|
||||
for (unsigned i = 0; i < sizeof(points) / sizeof(points[0]); i++) {
|
||||
sql(f.inst->topo_sqlite_db, query); fault = points[i];
|
||||
int rc = chat_attachment_bind(f.inst, f.channel, f.id, message, hash);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "binding test: fault=%d rc=%d consumed=%d events=%u", points[i], rc, fault == NONE, f.events);
|
||||
assert(rc < 0 && fault == NONE && f.events == 1 && sqlite3_get_autocommit(f.inst->topo_sqlite_db));
|
||||
binding_state(&f, message, hash, 0); committed(&f, 0);
|
||||
assert(!chat_attachment_bind(f.inst, f.channel, f.id, message, hash)); binding_state(&f, message, hash, 1);
|
||||
assert(!chat_attachment_bind(f.inst, f.channel, f.id, message, hash)); binding_state(&f, message, hash, 1);
|
||||
}
|
||||
uint8_t conflicting[32]; memcpy(conflicting, hash, 32); conflicting[0] ^= 1;
|
||||
assert(chat_attachment_bind(f.inst, f.channel, f.id, message, conflicting) < 0); binding_state(&f, message, hash, 1);
|
||||
sql(f.inst->topo_sqlite_db, "BEGIN;UPDATE attachment_keys SET ready=8 WHERE channel='900'");
|
||||
assert(chat_attachment_bind(f.inst, f.channel, f.id, message, hash) < 0 && !sqlite3_get_autocommit(f.inst->topo_sqlite_db));
|
||||
binding_state(&f, message, hash, 1); sql(f.inst->topo_sqlite_db, "ROLLBACK"); committed(&f, 1);
|
||||
fault_prefix = NULL; cleanup(&f);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS attachment binding:13 SQL faults/retries, exact committed identity, idempotent repeat, conflicting hash and foreign transaction rejected");
|
||||
}
|
||||
|
||||
static void test_attachment_identity(void) {
|
||||
const char* name = "00112233445566778899aabbccddeeff.uenc";
|
||||
char body[55] = "*"; uint8_t id[16], unchanged[16]; memset(unchanged, 0xa5, sizeof(unchanged));
|
||||
assert(b64_encode((const uint8_t*)name, strlen(name), body + 1, 52) == 52); body[53] = '|'; body[54] = 0;
|
||||
assert(!chat_attachment_identity(body, 54, id));
|
||||
for (unsigned i = 0; i < 16; i++) assert(id[i] == i * 17);
|
||||
const char* alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
for (int i = 49; i < 64; i++) {
|
||||
body[50] = alphabet[i]; memcpy(id, unchanged, 16);
|
||||
assert(chat_attachment_identity(body, 54, id) < 0 && !memcmp(id, unchanged, 16));
|
||||
}
|
||||
body[50] = 'w'; body[51] = '!'; memcpy(id, unchanged, 16);
|
||||
assert(chat_attachment_identity(body, 54, id) < 0 && !memcmp(id, unchanged, 16));
|
||||
body[51] = '='; body[52] = '!'; assert(chat_attachment_identity(body, 54, id) < 0);
|
||||
body[52] = '='; assert(chat_attachment_identity(body, 54, NULL) < 0);
|
||||
assert(chat_attachment_identity(NULL, 54, id) < 0);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS attachment identity: canonical base64 only, 15 trailing-bit aliases and invalid padding rejected, output preserved on error");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_ERROR); debug_set_category_level(DEBUG_CATEGORY_MEDIA, DEBUG_LEVEL_INFO);
|
||||
utun_instance_set_tun_init_enabled(0); size_t baseline = u_get_allocated_count();
|
||||
@@ -616,6 +676,7 @@ int main(void) {
|
||||
test_decryption_claim(); test_decryption_completion(); test_completion_controls(); test_completion_stop();
|
||||
test_worker_error_completion(); test_completion_limit();
|
||||
test_channel_delete_rollback();
|
||||
test_attachment_binding(); test_attachment_identity();
|
||||
assert(u_get_allocated_count() == baseline);
|
||||
DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "PASS attachment: independent committed key/metadata before worker, allocations restored");
|
||||
return 0;
|
||||
|
||||
Reference in new issue
Block a user