From 92d52d25a880de65214faed21649aa48183106b4 Mon Sep 17 00:00:00 2001 From: evgeny Date: Sat, 24 Oct 2026 14:40:53 +0200 Subject: [PATCH] Prepare encrypted attachments in exclusively owned durable temporaries --- src/media_async/encrypted_file.c | 128 ++++++++++++------------------- src/media_async/encrypted_file.h | 21 ++--- 2 files changed, 61 insertions(+), 88 deletions(-) diff --git a/src/media_async/encrypted_file.c b/src/media_async/encrypted_file.c index 8e50445f..ad61a226 100644 --- a/src/media_async/encrypted_file.c +++ b/src/media_async/encrypted_file.c @@ -10,118 +10,85 @@ #include #include #include -#include -#ifdef _WIN32 -#include -#endif - -/* Сделать создание/удаление имён durable на POSIX. */ -int encrypted_file_sync_directory(const char* path) { -#ifndef _WIN32 - char parent[1024]; - snprintf(parent, sizeof(parent), "%s", path); - char* slash = strrchr(parent, '/'); - if (!slash) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: path without parent: %s", path); return -1; } - *slash = '\0'; - int fd = open(parent, O_RDONLY | O_DIRECTORY); - if (fd < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: directory open failed: %s: %s", parent, strerror(errno)); return -1; } - int rc = fsync(fd); - if (rc) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: directory sync failed: %s: %s", parent, strerror(errno)); - close(fd); - return rc ? -1 : 0; -#else - (void)path; - return 0; -#endif -} - -/* Windows MoveFileEx обеспечивает замену и write-through существующего артефакта. */ -int encrypted_file_publish(const char* temp, const char* path) { -#ifdef _WIN32 - if (!MoveFileExA(temp, path, MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: publish failed %s -> %s winerr=%lu", temp, path, GetLastError()); - return -1; - } -#else - if (rename(temp, path) != 0) { - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: publish failed %s -> %s: %s", temp, path, strerror(errno)); - return -1; - } -#endif - return encrypted_file_sync_directory(path); -} #define EF_HEADER 66 #define EF_META (32 + ATTACHMENT_WIRE_MAX) -/* Гарантировать сохранение файла перед публикацией или подтверждением. */ -int encrypted_file_sync(const char* path) { - FILE* f = fopen(path, "rb+"); - if (!f) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: sync open failed path=%s: %s", path, strerror(errno)); return -1; } -#ifdef _WIN32 - int rc = _commit(_fileno(f)); -#else - int rc = fsync(fileno(f)); -#endif - if (fclose(f)) rc = -1; - if (rc) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: sync failed path=%s: %s", path, strerror(errno)); - return rc ? -1 : 0; -} - -/* Записать контейнер; частичный destination удаляется при любой ошибке. */ -int encrypted_file_write(const char* source, const char* destination, const uint8_t key[32], +/* Подготовить контейнер в эксклюзивном temp; конечный target не открывается для записи. */ +int encrypted_file_write(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32], uint64_t author, const uint8_t id[16], const struct attachment_info* info, const char* type) { + if (!output || output->file || output->temporary[0]) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: write requires inactive output owner"); return -1; + } FILE* in = NULL; FILE* out = NULL; int rc = -1; uint8_t header[EF_HEADER] = {0}, meta[EF_META] = {0}, cipher[EF_META + 16], nonce[13], hash[32]; size_t length = 0; uint64_t size = 0; - if (!source || !destination || !strcmp(source, destination) || !key || !author || !id || + const char* reason = "invalid arguments"; + if (!source || !target || !key || !author || !id || !type || strlen(type) >= 32 || attachment_validate(info)) goto done; + reason = "source/target alias or invalid file type"; + if (file_io_paths(source, target)) goto done; + reason = "cannot open source"; + in = file_io_open_read(source); + if (!in) goto done; + reason = "metadata encoding/encryption failed"; int n = attachment_encode(info, meta + 32, ATTACHMENT_WIRE_MAX); if (n < 0) goto done; memcpy(meta, type, strlen(type)); media_cipher_nonce(author, id, UINT64_MAX, nonce); if (media_ccm_encrypt(key, nonce, meta, (size_t)n + 32, cipher, &length)) goto done; - in = fopen(source, "rb"); out = fopen(destination, "wb"); - if (!in || !out) goto done; + reason = "cannot create private output"; + if (file_io_temp_open(output, target, FILE_IO_DURABLE)) goto done; + out = output->file; + reason = "container write/encryption failed"; memcpy(header, "UTENC001", 8); memcpy(header + 8, id, 16); header[32] = (uint8_t)(length >> 8); header[33] = (uint8_t)length; if (fwrite(header, 1, sizeof(header), out) != sizeof(header) || fwrite(cipher, 1, length, out) != length || media_encrypt_stream(key, author, id, in, out, &size, hash)) goto done; for (unsigned i = 0; i < 8; i++) header[24 + i] = (uint8_t)(size >> (56 - i * 8)); memcpy(header + 34, hash, 32); - if (fseeko(out, 0, SEEK_SET) || fwrite(header, 1, sizeof(header), out) != sizeof(header) || fflush(out)) goto done; + reason = "cannot rewrite container header"; + if (fseeko(out, 0, SEEK_SET) || fwrite(header, 1, sizeof(header), out) != sizeof(header)) goto done; + reason = "source close or output durable finish failed"; rc = 0; done: OPENSSL_cleanse(meta, sizeof(meta)); if (in && fclose(in)) rc = -1; - if (out && fclose(out)) rc = -1; - if (!rc) rc = encrypted_file_sync(destination); + if (!rc) rc = file_io_temp_finish(output); if (rc) { - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: preparation failed source=%s: %s", source ? source : "?", strerror(errno)); - if (out && remove(destination) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "encrypted file: partial cleanup failed"); - } else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: prepared author=%llu bytes=%llu", (unsigned long long)author, - (unsigned long long)size); + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: preparation rejected source=%s target=%s reason=%s", + source ? source : "?", target ? target : "?", reason); + file_io_temp_discard(output); + } else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: prepared author=%llu bytes=%llu temp=%s", + (unsigned long long)author, (unsigned long long)size, output->temporary); return rc; } /* Проверить подписанный hash, метаданные и все порции до разрешения публикации. */ -int encrypted_file_read(const char* source, const char* destination, const uint8_t key[32], uint64_t author, +int encrypted_file_read(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32], uint64_t author, const uint8_t id[16], const uint8_t hash[32], struct attachment_info* info, char type[32]) { + if (!output || output->file || output->temporary[0]) { + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: read requires inactive output owner"); return -1; + } FILE* in = NULL; FILE* out = NULL; + struct attachment_info result; + char result_type[32]; int rc = -1; uint8_t actual[32], header[EF_HEADER], meta[EF_META], cipher[EF_META + 16], nonce[13]; size_t length = 0; uint64_t size = 0; const char* reason = "invalid arguments"; - if (!source || !destination || !strcmp(source, destination) || !key || !author || !id || !hash || !info || !type) goto done; + if (!source || !target || !key || !author || !id || !hash || !info || !type) goto done; + reason = "source/target alias or invalid file type"; + if (file_io_paths(source, target)) goto done; reason = "cannot hash ciphertext"; if (ma_sha256_file(source, actual)) goto done; reason = "ciphertext does not match signed manifest hash"; if (CRYPTO_memcmp(actual, hash, 32)) goto done; reason = "cannot open ciphertext or truncated header"; - in = fopen(source, "rb"); + in = file_io_open_read(source); if (!in || fread(header, 1, sizeof(header), in) != sizeof(header)) goto done; reason = "unsupported container format or mismatched UUID"; if (memcmp(header, "UTENC001", 8) || memcmp(header + 8, id, 16)) goto done; @@ -134,14 +101,14 @@ int encrypted_file_read(const char* source, const char* destination, const uint8 if (media_ccm_decrypt(key, nonce, cipher, encrypted_size, meta, &length)) goto done; reason = "invalid attachment metadata"; if (length < 32 || - !memchr(meta, 0, 32) || !meta[0] || attachment_decode(meta + 32, length - 32, info)) goto done; + !memchr(meta, 0, 32) || !meta[0] || attachment_decode(meta + 32, length - 32, &result)) goto done; reason = "invalid content type"; for (unsigned i = 0; i < 32 && meta[i]; i++) if (meta[i] < 33 || meta[i] > 126 || meta[i] == '"' || meta[i] == '\\') goto done; - memcpy(type, meta, 32); + memcpy(result_type, meta, 32); for (unsigned i = 0; i < 8; i++) size = (size << 8) | header[24 + i]; - out = fopen(destination, "wb"); reason = "cannot open temporary plaintext"; - if (!out) goto done; + if (file_io_temp_open(output, target, FILE_IO_DURABLE)) goto done; + out = output->file; reason = "file authentication, size or body hash failed"; if (media_decrypt_stream(key, author, id, size, header + 34, in, out)) goto done; reason = "close or durable file sync failed"; @@ -149,12 +116,15 @@ int encrypted_file_read(const char* source, const char* destination, const uint8 done: OPENSSL_cleanse(meta, sizeof(meta)); if (in && fclose(in)) rc = -1; - if (out && fclose(out)) rc = -1; - if (!rc) rc = encrypted_file_sync(destination); + if (!rc) rc = file_io_temp_finish(output); if (rc) { - DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected source=%s reason=%s", source ? source : "?", reason); - if (out && remove(destination) && errno != ENOENT) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected output cleanup failed"); - } else DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: verified author=%llu bytes=%llu", (unsigned long long)author, - (unsigned long long)size); + DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "encrypted file: rejected source=%s target=%s reason=%s", + source ? source : "?", target ? target : "?", reason); + file_io_temp_discard(output); + } else { + *info = result; memcpy(type, result_type, sizeof(result_type)); + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "encrypted file: verified author=%llu bytes=%llu temp=%s", + (unsigned long long)author, (unsigned long long)size, output->temporary); + } return rc; } diff --git a/src/media_async/encrypted_file.h b/src/media_async/encrypted_file.h index 7afc3438..0b6f9d2b 100644 --- a/src/media_async/encrypted_file.h +++ b/src/media_async/encrypted_file.h @@ -1,23 +1,26 @@ /* Контейнер группового вложения: публичный заголовок, зашифрованные метаданные и * порции AES-256-CCM. Полный SHA256 контейнера берётся из подписанного сообщения. - * Все функции файловые, вызываются только в media_async worker. Выход decrypt — - * временный файл; публиковать его можно только после успеха и durable сохранения. */ + * Вызывать только в media_async worker. Write/read готовят отдельный durable temp, + * конечное имя публикует владелец через file_io_temp_publish. Source не изменяется. */ #ifndef ENCRYPTED_FILE_H #define ENCRYPTED_FILE_H #include "attachment.h" +#include "../../lib/file_io.h" #ifdef __cplusplus extern "C" { #endif #define ENCRYPTED_ATTACHMENT_TYPE "application/x-utun-encrypted" -int encrypted_file_write(const char* source, const char* destination, const uint8_t key[32], +/* source/target — UTF-8 пути разных файлов; target отсутствует либо regular без конечной ссылки. + * output заранее обнулён и имеет стабильный адрес до publish/discard; активный output отклоняется без очистки. + * 0 — output содержит закрытый проверенный temp рядом с target, target ещё не изменён. + * -1 — ошибка; собственный temp удалён, source/target сохранены. Ключ32 байта, UUID16, author!=0. + * Пути и файлы не подменяются во время вызова; публикация повторно проверяет их идентичность. */ +int encrypted_file_write(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32], uint64_t author, const uint8_t id[16], const struct attachment_info* info, const char* type); -int encrypted_file_read(const char* source, const char* destination, const uint8_t key[32], uint64_t author, +/* Проверить подписанный hash всего контейнера, UUID, метаданные и все tags. Контракт output — как у write. + * info/type[32] присваиваются только при успехе; при ошибке прежние значения сохраняются. */ +int encrypted_file_read(const char* source, const char* target, struct file_io_temp* output, const uint8_t key[32], uint64_t author, const uint8_t id[16], const uint8_t hash[32], struct attachment_info* info, char type[32]); -/* Flush/fsync и close проверяет владелец; этот примитив сохраняет уже закрытый файл. */ -int encrypted_file_sync(const char* path); -/* Durable rename (write-through на Windows) и сохранение каталога на POSIX. */ -int encrypted_file_publish(const char* temporary, const char* path); -int encrypted_file_sync_directory(const char* path); #ifdef __cplusplus } #endif