Криптография (общая)
Одинакова для wire 1.0 и 1.1. Меняется только формат кадра после рукопожатия.
- ECDH — X25519, свежая пара ключей на сессию
- KDF — HKDF-SHA256, salt пустой; info
DVeProto-v1/c2sиDVeProto-v1/s2c - AEAD — AES-256-GCM, nonce 12 байт на каждое сообщение
- Клиент → сервер: ключ c2s. Сервер → клиент: ключ s2c.
Рукопожатие
Всегда UTF-8 JSON, одинаково для обеих версий. Поле v рукопожатия = 1.
- Сервер:
dve_helloсserver_pkи опциональноoffer: ["1.0","1.1"]. - Клиент считает shared secret X25519 и ключи HKDF.
- Клиент:
dve_client_ackсclient_pkиselect: "1.0"или"1.1". Безselect→ 1.0. - Дальше только выбранный wire-формат.
{
"type": "dve_hello",
"proto": "DVeProto",
"v": 1,
"server_pk": "<base64 32 bytes>",
"offer": ["1.0", "1.1"]
}
{
"type": "dve_client_ack",
"proto": "DVeProto",
"v": 1,
"client_pk": "<base64 32 bytes>",
"select": "1.1"
}
Версии wire
Выберите версию, чтобы посмотреть формат данных после рукопожатия.
Wire 1.0 — JSON text frames
После handshake каждое сообщение — один WebSocket text frame с JSON. Ciphertext и nonce в Base64.
- Транспорт: только text frames
- Обёртка:
type: "dve",proto,v: 1,n,c - Plaintext внутри AEAD — UTF-8 JSON объект
- Режим по умолчанию, если клиент не указал select
{
"type": "dve",
"proto": "DVeProto",
"v": 1,
"n": "<base64 12-byte nonce>",
"c": "<base64 AES-GCM ciphertext||tag>"
}
Подходит для каналов без binary frames и для максимальной совместимости со старыми клиентами.
Wire 1.1 — binary frames
После handshake данные идут только binary WebSocket frames. Без JSON и Base64 на wire.
- Транспорт: только binary frames (opcode 0x2)
- Длина сообщения = граница кадра
verвсегда0x11- Чанковая передача файлов (FILE_*)
[1] ver = 0x11 [1] ptype [12] nonce […] ciphertext || GCM-tag(16)
ptype
| ptype | Имя | Назначение |
|---|---|---|
0x01 | APP_JSON | UTF-8 JSON / RPC |
0x02 | APP_BIN | Произвольные байты |
0x03 | TEXT | UTF-8 текст |
0x10 | FILE_BEGIN | Старт файла |
0x11 | FILE_CHUNK | Чанк |
0x12 | FILE_END | Конец |
0x13 | FILE_ABORT | Отмена |
0x14 | FILE_ACK | ACK / resume |
0x15 | FILE_RESUME | Продолжить с чанка |
Файлы
FILE_BEGIN → FILE_CHUNK* → FILE_END. Каждый chunk — отдельный binary frame со своим nonce/tag. Resume: FILE_RESUME / FILE_ACK. Чанк по умолчанию 256 KiB.
FILE_BEGIN: transfer_id[16] | file_size u64be | chunk_size u32be | total_chunks u32be | flags u8 | name_len u16be | name | mime_len u16be | mime | [sha256 32 if flags&1] FILE_CHUNK: transfer_id[16] | chunk_index u32be | data FILE_END: transfer_id[16] | sha256[32] FILE_ABORT: transfer_id[16] | reason_code u8 FILE_ACK: transfer_id[16] | status u8 | last_ok u32be FILE_RESUME: transfer_id[16] | from_chunk u32be
Эталонный код
Референс-реализации в этой же папке — без привязки к конкретному продукту.
Python
DVeClientSession.from_server_hello_text→ session + ack JSONpack_outgoing/unpack_incoming—str(1.0) илиbytes(1.1)- Хелперы
pack_file_*·pip install cryptography
session, ack = DVeClientSession.from_server_hello_text(hello_text, prefer="1.1")
frame = session.pack_outgoing({"dve_op": "ping"})
Браузер (ESM)
dveDeveloperHandshake→{ session, clientAckJson, wire }- Для 1.1
encryptOutgoing→Uint8Array
import { dveDeveloperHandshake } from './reference-client.js';
const { session, clientAckJson, wire } = await dveDeveloperHandshake(hello);
const out = await session.encryptOutgoing({ dve_op: 'ping' });
Конвенция APP_JSON
dve_op— имя операции / RPCdve_seq— опциональный монотонный счётчик (DVeSeq)
Совместимость
Клиенты без select работают на 1.0. Если обе стороны умеют 1.1 — выбирают binary. Смешанные пары остаются на JSON-кадрах.