Prepare encrypted attachments in exclusively owned durable temporaries
This commit is contained in:
1 parent
9e0949434e
commit
92d52d25a8
2 files changed
+61
-88
No files matched your search
@@ -10,118 +10,85 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user