Шифрование пакетов MOP, версия 1 (нормативная спецификация)
Реализация:
kernel/crates/meridian-crypto(сессия B). Векторы:spec/formats/vectors/mop-encryption-v1.json; независимая проверка на Python:kernel/crates/meridian-crypto/tests/reference/mop_reference.py(совпадает байт в байт). Уточняетdocs/04-file-formats.md§2.16 (владелец — сессия A; запрос на синхронизацию — во «Входящих» A). Основа — собственный код владельца PasswordSafe (crypto.rs,vault/envelope.rs, веткаwip/snapshot-2026-10-04), с исправлениями из брифа B2: ключи через HKDF, MAC на весь файл, потоковое AES-256-GCM по чанкам, Argon2id по RFC 9106.
Ключевые слова «должен», «не должен», «может» — в смысле RFC 2119.
Все целые — big-endian. ‖ — конкатенация.
1. Контейнер
Зашифрованный документ — ZIP с тремя записями:
| Запись | Сжатие | Содержимое |
|---|---|---|
mimetype |
stored, первая | application/vnd.moffice.encrypted (без перевода
строки) |
package.bin |
stored | шифротекст исходного пакета MOP (§4) |
encryption.xml |
deflate или stored | заголовок (§3) |
Порядок package.bin и encryption.xml
свободный: заголовок готов только после записи всего шифротекста (в нём
размер и хэш), поэтому пишущий может положить его после
package.bin. Читатель находит записи по центральному
каталогу ZIP. thumbnail.png, meta.xml и прочие
части исходного пакета в открытом виде не хранятся.
2. Ключи
| Обозначение | Как получается | Назначение |
|---|---|---|
DEK |
32 байта CSPRNG ОС | ключ файла; никогда не используется напрямую |
file_id |
16 байт CSPRNG | соль HKDF, входит в AAD |
nonce_prefix |
7 байт CSPRNG | префикс nonce чанков |
K_data |
HKDF-SHA256(salt = file_id, IKM = DEK, info = "meridian-mop-v1/data-key", L = 32) |
AES-256-GCM чанков |
K_mac |
HKDF-SHA256(salt = file_id, IKM = DEK, info = "meridian-mop-v1/header-mac-key", L = 32) |
HMAC заголовка |
HKDF — RFC 5869 (Extract, затем Expand). Строки info —
ASCII без завершающего нуля.
2.1 Ключ-шифровальщик
«пароль» (m:password)
pw = UTF-8(NFC(пароль)) — пустой пароль запрещён
A = Argon2id(pw, salt (16 байт), m = memory КиБ, t = time, p = parallelism, T = 32, версия 0x13, без secret и AD)
KEK = HKDF-SHA256(salt = file_id, IKM = A, info = "meridian-mop-v1/kek/password")
wrapped-key = AES-256-GCM(KEK, nonce = wrap-nonce (12 байт), AAD = "meridian-mop-v1/wrap/password" ‖ file_id, DEK)
= 32 байта шифротекста ‖ 16 байт тега
Параметры по умолчанию — вторая рекомендация RFC 9106 §4:
memory = 65536 (64 МиБ), time = 3,
parallelism = 4. Профиль организации может задать первую:
memory = 2097152 (2 ГиБ), time = 1,
parallelism = 4.
- Пишущий не должен использовать
memory < 19456(19 МиБ) иtime = 1приmemory < 1 ГиБ. - Читающий должен отвергать
parallelism ∉ 1..=16,time ∉ 1..=16,memory ∉ 8·p..=4194304(4 ГиБ) — защита от DoS. - Неверный пароль обнаруживается по тегу
wrapped-keyдо чтения данных.
2.2
Ключ-шифровальщик «код восстановления»
(m:recovery-code)
Код — 20 байт CSPRNG (160 бит), показывается пользователю один раз в
виде 32 символов Crockford Base32
(0123456789ABCDEFGHJKMNPQRSTVWXYZ) группами по 4 через
дефис: XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX-XXXX. При вводе
регистр, пробелы и дефисы не важны; I, L →
1, O → 0.
KEK = HKDF-SHA256(salt = file_id, IKM = код (20 байт), info = "meridian-mop-v1/kek/recovery-code")
wrapped-key = AES-256-GCM(KEK, wrap-nonce, AAD = "meridian-mop-v1/wrap/recovery-code" ‖ file_id, DEK)
Argon2id не нужен: код имеет полную энтропию. Перенос идеи из PasswordSafe (ключ восстановления хранилища).
2.3 Будущие виды
m:certificate (RSA-OAEP-SHA256) и HPKE X25519 для
корпоративного восстановления — в следующих версиях. Читатель
должен сохранять неизвестные элементы внутри
m:key-encryptors как есть (имя и атрибуты): они входят в
MAC.
3. Заголовок
encryption.xml
<?xml version="1.0" encoding="UTF-8"?>
<m:encryption xmlns:m="urn:moffice:core:1" version="1" inner-mime="application/vnd.moffice.write">
<m:data cipher="AES-256-GCM" stream="stream-v1" chunk-size="4096" chunks="2" plain-size="5000"
nonce-prefix="b64:MDEyMzQ1Ng==" file-id="b64:ICEiIyQlJicoKSorLC0uLw==" package-sha256="b64:…"/>
<m:key-encryptors>
<m:password id="pw1" kdf="argon2id" kdf-version="19" memory="65536" time="3" parallelism="4"
salt="b64:…" wrap-nonce="b64:…" wrapped-key="b64:…"/>
<m:recovery-code id="rc1" wrap-nonce="b64:…" wrapped-key="b64:…"/>
</m:key-encryptors>
<m:mac alg="HMAC-SHA256" value="b64:…"/>
</m:encryption>Правила разбора (читающий должен их соблюдать):
- Размер ≤ 64 КиБ, UTF-8, без DTD, сущностей, инструкций обработки, текста и CDATA; комментарии допускаются.
- Все элементы — в пространстве имён
urn:moffice:core:1(префикс любой); атрибуты без префикса. - Порядок:
m:encryption→m:data→m:key-encryptors(1…32 пустых элемента) →m:mac. Лишние или повторные элементы и атрибуты — ошибка. Значение атрибута ≤ 4096 байт. - Числа — десятичные без знака и ведущих нулей. Двоичные значения —
b64:+ Base64 (RFC 4648, с дополнением) точной длины:nonce-prefix7,file-id16,package-sha25632,salt16,wrap-nonce12,wrapped-key48,mac32 байта. version = 1;cipher = AES-256-GCM;stream = stream-v1;kdf = argon2id;kdf-version = 19; иначе — «не поддерживается».chunk-size ∈ 4096..=67108864;chunks = max(1, ⌈plain-size / chunk-size⌉).inner-mime— тип исходного пакета ([a-z0-9.+/-], ≤ 255 символов); вложенное шифрование запрещено.idключей-шифровальщиков уникальны,[A-Za-z0-9_-]{1,64}.
3.1 MAC заголовка («MAC на весь файл»)
MAC считается по канонической последовательности атрибутов, а не по тексту XML (переформатирование и смена префикса пространства имён MAC не ломают):
str(x) = u32(длина UTF-8 x) ‖ UTF-8 x
elem(e) = str(локальное имя e) ‖ u32(число атрибутов) ‖ для атрибутов в порядке возрастания имени (по байтам): str(имя) ‖ str(значение)
canon = "meridian-mop-v1/header" ‖ elem(m:encryption) ‖ elem(m:data) ‖ elem(каждый ключ-шифровальщик по порядку документа)
mac = HMAC-SHA256(K_mac, canon)
Значения атрибутов — после раскрытия ссылок XML и нормализации
атрибутов XML 1.0. Атрибуты xmlns/xmlns:* в
каноническую форму не входят. Поскольку m:data содержит
plain-size, chunks и
package-sha256 (SHA-256 всего
package.bin), MAC заголовка удостоверяет весь файл:
параметры, ключи-шифровальщики, тип пакета и шифротекст.
4. Данные
package.bin (конструкция STREAM)
Исходный пакет P делится на чанки по
chunk-size байт (по умолчанию 4 МиБ); последний чанк — от 0
до chunk-size байт; пустой пакет — один пустой чанк. Для
чанка i (с нуля) из n:
nonce_i = nonce_prefix (7) ‖ u32(i) ‖ u8(i = n − 1 ? 1 : 0)
AAD = "meridian-mop-v1/chunk" ‖ file_id
C_i = AES-256-GCM(K_data, nonce_i, AAD, P_i) — шифротекст ‖ тег (16 байт)
package.bin = C_0 ‖ C_1 ‖ … ‖ C_{n−1}
Флаг последнего чанка в nonce защищает от усечения и дописывания, номер — от перестановки (Hoang, Reyhanitabar, Rogaway, Vizár, 2015). Пишущему не нужно знать размер заранее: последний чанк определяется заглядыванием вперёд. Число чанков ограничено 2³².
5. Порядок открытия
- Разобрать
encryption.xmlпо §3 (пределы — до любых вычислений). - Для пароля — перебрать
m:passwordпо порядку: вычислить KEK, развернутьwrapped-key; первый успешный даётDEK. Ни один не подошёл — «неверный пароль»; ни одного элемента нужного вида — «нет ключа-шифровальщика». - Вывести
K_data,K_mac; сверитьmac(сравнение за постоянное время). Не совпал — «заголовок изменён». - Расшифровать чанки потоком, выдавая открытый текст чанка только
после проверки его тега; параллельно считать SHA-256 всего
package.bin. - Сверить число чанков,
plain-sizeиpackage-sha256. Пока проверка не завершена, результат не должен использоваться (пишется во временный файл, переименование — после успеха).
6. Операции без перешифровки данных
- Смена пароля: развернуть
DEKстарым секретом, заменить всеm:passwordновым (новые соль иwrap-nonce), пересчитатьmac. Код восстановления сохраняется. - Добавление кода восстановления / сертификата: то же, с добавлением элемента.
- Повторное шифрование при сохранении документа
должно использовать новые
DEK,file_idиnonce_prefix.
7. Тестовые векторы
spec/formats/vectors/mop-encryption-v1.json — три случая
(два чанка с кодом восстановления и паролем в разложенной форме Unicode;
пустой документ; размер ровно на границе чанка). Открытый текст:
P[i] = i mod 251. «Случайные» байты — последовательность
00 01 02 …, расходуемая в порядке: DEK (32),
file_id (16), nonce_prefix (7), соль (16),
wrap-nonce (12), [код восстановления (20), его
wrap-nonce (12)]. Векторы проверяются тестом крейта
(cargo test -p meridian-crypto --test vectors) и
независимой реализацией на Python
(tests/reference/mop_reference.py, библиотеки
cryptography и argon2-cffi).
8. Отличия от
docs/04-file-formats.md §2.16 (черновика)
| Было | Стало | Почему |
|---|---|---|
| Ключ данных = DEK напрямую | K_data, K_mac из DEK через HKDF |
разделение ключей по назначению |
| KEK = выход Argon2id напрямую | KEK = HKDF(выход Argon2id) | то же; привязка к file_id |
| Заголовок без MAC | HMAC-SHA256 по канонической форме, включая хэш всего
package.bin |
целостность параметров, ключей-шифровальщиков и файла |
nonce-prefix 8 байт ‖ u32(i); AAD с chunks
и plain-size |
префикс 7 байт ‖ u32(i) ‖ флаг последнего; AAD с
file_id |
STREAM: пишущему не нужен размер заранее, усечение ловится флагом |
Атрибут aad |
stream="stream-v1" |
алгоритм задаётся версией, а не строкой AAD |
| Только пароль и сертификат | + код восстановления (из PasswordSafe) | офлайн-восстановление без организации |
9. Безопасность и ограничения
- Все секреты в памяти затираются (
zeroize); ключи не печатаются (Debugскрывает значение). - Сравнение MAC — за постоянное время (
subtle). - Шифрование защищает содержимое файла, но не скрывает его примерный
размер и тип пакета (
inner-mime). - Ключи подписи документов (XAdES) и шифрование для получателей по сертификату — отдельные механизмы (§2.17 форматов, будущие версии этой спецификации).