В криптографических системах с асимметричными ключами возникает фундаментальное ограничение: один зашифрованный блок данных, созданный с использованием публичного ключа, может быть расшифрован только соответствующим приватным ключом. Это удобно для сценариев «один отправитель — один получатель», но становится неудобным, когда сообщение должно быть доставлено группе участников.
Решение этой задачи строится на гибридной схеме, где используется сочетание симметричного и асимметричного шифрования. В контексте TweetNaCl.js / nacl.js такая модель часто реализуется как «конверт шифрования» (envelope encryption).
Конверт состоит из двух уровней:
Данные шифруются симметричным ключом
nacl.secretbox)Симметричный ключ шифруется для каждого получателя отдельно
nacl.box или
nacl.box.sealИтоговая структура:
Symmetric encryption (secretbox) значительно быстрее, чем асимметрическое шифрование. Но secretbox требует общего секретного ключа.
Ассиметрическая криптография (box) позволяет безопасно передавать ключ, но дорогая по вычислениям.
Поэтому:
Основные функции:
nacl.randomBytes(n) — генерация случайных данныхnacl.secretbox(message, nonce, key) — симметричное
шифрованиеnacl.secretbox.open(cipher, nonce, key) —
расшифрованиеnacl.box(message, nonce, publicKey, secretKey) —
асимметричное шифрованиеnacl.box.open(cipher, nonce, publicKey, secretKey) —
расшифрованиеnacl.box.keyPair() — генерация пары ключейnacl.box.seal() — упрощённое шифрование без nonce (NaCl
sealed box)Практическая реализация обычно сериализует конверт в JSON или бинарный формат:
{
"ciphertext": "base64...",
"nonce": "base64...",
"keys": [
{
"recipient": "userA",
"encryptedKey": "base64..."
},
{
"recipient": "userB",
"encryptedKey": "base64..."
}
]
}
import nacl from "tweetnacl";
const symmetricKey = nacl.randomBytes(nacl.secretbox.keyLength);
Этот ключ будет использоваться для шифрования основного сообщения.
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
const messageUint8 = new TextEncoder().encode("Секретное сообщение для группы");
const ciphertext = nacl.secretbox(messageUint8, nonce, symmetricKey);
Каждый получатель имеет ключевую пару:
const alice = nacl.box.keyPair();
const bob = nacl.box.keyPair();
const carol = nacl.box.keyPair();
Здесь используется nacl.box. Для этого нужен ephemeral
key sender-а:
const sender = nacl.box.keyPair();
Теперь шифруем ключ:
function encryptKeyForRecipient(recipientPublicKey, keyToEncrypt, senderSecretKey) {
const nonce = nacl.randomBytes(nacl.box.nonceLength);
const encrypted = nacl.box(
keyToEncrypt,
nonce,
recipientPublicKey,
senderSecretKey
);
return {
nonce,
encrypted
};
}
const recipients = [
{ id: "alice", key: alice.publicKey },
{ id: "bob", key: bob.publicKey },
{ id: "carol", key: carol.publicKey }
];
const encryptedKeys = recipients.map(r => {
const { nonce, encrypted } = encryptKeyForRecipient(
r.key,
symmetricKey,
sender.secretKey
);
return {
recipient: r.id,
nonce,
encryptedKey: encrypted
};
});
const envelope = {
senderPublicKey: sender.publicKey,
nonce,
ciphertext,
encryptedKeys
};
Получатель выполняет обратные действия:
function findKey(envelope, recipientId) {
return envelope.encryptedKeys.find(k => k.recipient === recipientId);
}
const keyPacket = findKey(envelope, "alice");
const symmetricKey = nacl.box.open(
keyPacket.encryptedKey,
keyPacket.nonce,
envelope.senderPublicKey,
alice.secretKey
);
const decrypted = nacl.secretbox.open(
envelope.ciphertext,
envelope.nonce,
symmetricKey
);
const message = new TextDecoder().decode(decrypted);
TweetNaCl.js поддерживает упрощённый режим:
const encryptedKey = nacl.box.seal(symmetricKey, recipientPublicKey);
Минус:
Плюс:
При десятках и сотнях участников возникают проблемы:
Решения:
TweetNaCl работает с Uint8Array, поэтому требуется
кодирование:
function encodeBase64(u8) {
return Buffer.from(u8).toString("base64");
}
function decodeBase64(str) {
return new Uint8Array(Buffer.from(str, "base64"));
}
Применение:
const serialized = {
ciphertext: encodeBase64(ciphertext),
nonce: encodeBase64(nonce),
encryptedKeys: encryptedKeys.map(k => ({
recipient: k.recipient,
nonce: encodeBase64(k.nonce),
encryptedKey: encodeBase64(k.encryptedKey)
}))
};
Конверт шифрования обеспечивает:
Если приватный ключ одного участника скомпрометирован:
В secretbox это критическая ошибка:
Для box каждый ключевой пакет должен иметь свой
nonce.
Важно проверять:
В реальных системах конверт часто используется в:
Типичная схема:
Конверт часто дополняется:
nacl.sign)Пример:
const signed = nacl.sign(
messageUint8,
senderSigningSecretKey
);
При росте числа получателей:
Оптимизации:
Конверт шифрования в TweetNaCl.js можно рассматривать как композицию двух уровней:
Такая структура остаётся базовым строительным блоком большинства современных криптографических протоколов, построенных поверх NaCl.