Я хочу использовать BouncyCastle для имитации схемы AEAD по умолчанию libsodium (X25519 ECDH, за которой следует симметричный шифр XSalsa20 с Poly1305 KDF).
Мне удалось выполнить DH с X25519 KeyAgreement
, чтобы сгенерировать javax.crypto.SecretKey
.
Но что касается следующего шага, я не знаю, что делать. BouncyCastle предлагает реализацию класса ChaCha20Poly1305
AEADCipher
, который, похоже, выполняет оба шага одновременно (генерацию MAC и шифрование). Однако я не нашел такого эквивалента для «XSalsa20Poly1305».
В BC есть Poly1305KeyGenerator
, но я не вижу способа передать ему nonce, поскольку он принимает только простые KeyGenerationParameters
объекты при инициализации.
Почему комбинация ChaCha20 и Poly1305 настолько особенная (по сравнению с [X]Salsa20+Poly1305), что у нее есть специальная реализация? Как я могу сделать то же самое для XSalsa20 с примитивами BouncyCastle?
Libsodium применяет XSalsa20-Poly1305 для шифрования с аутентификацией, которое реализовано в функции crypto_secretbox_easy() . Базовый алгоритм XSalsa20-Poly1305 можно прочитать в эталонной реализации:
k
и nonce n
в качестве входных параметров, получается ключевой подраздел.n
в качестве nonce (далее называемых subnonce), конкатенация пустого 32-байтового массива и открытого текста m
длиной mlen
шифруется.c
.mac
получается для c
применения ключа Poly1305.Более подробную информацию об алгоритмах, в частности HSalsa20, см., например. Расширение nonce Salsa20 и аутентификация, см., например. Поли1305.
BouncyCastle реализует XSalsa20Engine как производное от Salsa20Engine
. XSalsa20Engine
перезаписывает setKey() , который реализует HSalsa20 и внутренне определяет подраздел и субнонс. Когда экземпляр XSalsa20Engine
инициализируется, Salsa20Engine#init() выполняется и, таким образом, setKey()
.
Это позволяет реализовать XSalsa20-Poly1305 с помощью Bouncycastle следующим образом:
XSalsa20Engine
с использованием ключа и одноразового номера. Это внутренне вычисляет подразделы и субнонсы.Кроме того, генерируется случайный одноразовый номер (это делается в эталонной реализации перед вызовом crypto_secretbox_easy()
), а одноразовый номер, Poly1305 MAC и зашифрованный текст объединяются в этом порядке.
Что касается ключа Poly1305, обратите внимание на следующее:
macKey
в приведенной выше процедуре (пока) не зафиксировано, это происходит только неявно во время инициализации Poly1305 (в init() через setKey()).macKey
с помощью checkKey() , зажим() должен быть выполнен первым (последующее неявное ограничение во время инициализации Poly1305 тогда не имеет никакого эффекта).generateKey()
предназначен для генерации случайных ключей Poly1305, при этом ключ Poly1305, сгенерированный в контексте XSalsa20-Poly1305, генерируется детерминированно (способом, описанным выше).Пример реализации шифрования с помощью XSalsa20-Poly1305:
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HexFormat;
import org.bouncycastle.crypto.engines.XSalsa20Engine;
import org.bouncycastle.crypto.macs.Poly1305;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.util.Arrays;
...
static final int NONCEBYTES = 24;
static final int KEYBYTES = 32;
...
public static byte[] encrypt(byte[] key, byte[] plaintext) {
// generate random nonce
byte[] nonce = new byte[NONCEBYTES];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(nonce);
XSalsa20Engine xSalsa20Engine = new XSalsa20Engine();
xSalsa20Engine.init(true, new ParametersWithIV(new KeyParameter(key), nonce));
// generate mac key
byte[] macKey = new byte[KEYBYTES];
xSalsa20Engine.processBytes(macKey, 0, macKey.length, macKey, 0);
// encrypt plaintext
byte[] ciphertext = new byte[plaintext.length];
xSalsa20Engine.processBytes(plaintext, 0, plaintext.length, ciphertext, 0);
// generate mac
Poly1305 poly1305 = new Poly1305();
poly1305.init(new KeyParameter(macKey));
byte[] mac = new byte[poly1305.getMacSize()];
poly1305.update(ciphertext, 0, plaintext.length); // ciphertext size = plaintext size
poly1305.doFinal(mac, 0);
// concatenate, e.g. nonce|mac|ciphertext
return Arrays.concatenate(nonce, mac, ciphertext);
}
Пример расшифровки:
public static byte[] decrypt(byte[] key, byte[] nonceMacCiphertext) {
// separate nonce, mac and ciphertext
Poly1305 poly1305 = new Poly1305();
byte[] nonce = Arrays.copyOfRange(nonceMacCiphertext, 0, NONCEBYTES);
byte[] mac = Arrays.copyOfRange(nonceMacCiphertext, NONCEBYTES, NONCEBYTES + poly1305.getMacSize());
byte[] ciphertext = Arrays.copyOfRange(nonceMacCiphertext, NONCEBYTES + poly1305.getMacSize(), nonceMacCiphertext.length);
XSalsa20Engine xSalsa20Engine = new XSalsa20Engine();
xSalsa20Engine.init(false, new ParametersWithIV(new KeyParameter(key), nonce));
// generate mac key
byte[] macKey = new byte[KEYBYTES];
xSalsa20Engine.processBytes(macKey, 0, macKey.length, macKey, 0);
// calculate Mac
byte[] macCalculated = new byte[poly1305.getMacSize()];
poly1305.init(new KeyParameter(macKey));
poly1305.update(ciphertext, 0, ciphertext.length);
poly1305.doFinal(macCalculated, 0);
// decrypt on successful authentication
byte[] decrypted = null;
if (MessageDigest.isEqual(macCalculated, mac)) {
decrypted = new byte[ciphertext.length];
xSalsa20Engine.processBytes(ciphertext, 0, ciphertext.length, decrypted, 0);
}
return decrypted;
}
Тест:
Самый удобный способ проверить совместимость с XSalsa20-Poly1305 — сравнить зашифрованный текст, сгенерированный с помощью библиотеки Libsodium, например. Lazysodium и идентичные входные параметры:
import com.goterl.lazysodium.LazySodiumJava;
import com.goterl.lazysodium.SodiumJava;
import com.goterl.lazysodium.utils.Key;
...
public static void main(String[] args) throws Exception {
// Check XSalsa20-Poly1305 implementation
byte[] key = HexFormat.of().parseHex("a7e845b0854294da9aa743b807cb67b19647c1195ea8120369f3d12c70468f29");
byte[] plaintext = "The quick brown fox jumps over the lazy dog".getBytes(StandardCharsets.UTF_8);
byte[] nonceMacCiphertext = encrypt(key, plaintext);
System.out.println("CT, BouncyCastle: " + HexFormat.of().formatHex(nonceMacCiphertext));
byte[] decrypted = decrypt(key, nonceMacCiphertext);
System.out.println("Decrypted data: " + new String(decrypted, StandardCharsets.UTF_8));
// Compare nonce|mac|ciphertext with Lazysodium
byte[] nonce = Arrays.copyOfRange(nonceMacCiphertext, 0, NONCEBYTES);
SodiumJava sodium = new SodiumJava();
LazySodiumJava lazySodium = new LazySodiumJava(sodium, StandardCharsets.UTF_8);
String macCiphertext = lazySodium.cryptoSecretBoxEasy("The quick brown fox jumps over the lazy dog", nonce , Key.fromBytes(key));
System.out.println("CT, Lazysodium: " + HexFormat.of().formatHex(nonce) + macCiphertext.toLowerCase());
}
Оба зашифрованных текста совпадают для идентичных входных данных.
тогда почему проверка «детерминированного» ключа не проходит проверку Poly1305KeyGenerator#checkKey()
BouncyCastle? Похоже, что ключ не является валидным ключом Poly1305 — выдает IAE: Invalid format for r portion of Poly1305 key
. Я также просмотрел связанные технические документы [X]Salsa20, но там не упоминаются какие-либо ключи Poly1305, так почему же он должен вести себя по-другому при использовании с другими шифрами?
Кроме того, я нашел различные ссылки, в которых говорится: «Poly1305 принимает на вход 16-байтовый ключ (с некоторыми конкретными битами, установленными в ноль) и сообщение произвольной длины и выводит 16-байтовый дайджест сообщения. Выходные данные Poly1305 — это усечение (до 16 байт) полинома, вычисленного в F2130−5 по ключу r (который сначала «прижимает» определенные биты к нулю » eprint.iacr.org/2014/613.pdf Это не подходит); просто каждый раз просто устанавливая нулевой вход.
@Leprechaun - я отредактировал свой пост (см. часть перед реализацией примера) и ответил (среди прочего) на ваши вопросы из комментариев (особенно на вопросы о происхождении пустого 32-байтового массива, об ограничении и о размере ключа Poly1305). ). Это должно сделать реализацию BouncyCastle более прозрачной.
Ох, мне потребовалось много времени, чтобы переварить это, мне пришлось прочитать статьи Бернштейна, RFC ChaCha, код libsodium Rubberduck и перекрестные ссылки BC с вашим пошаговым обзором. Я думаю (?), что вы правы во всех своих утверждениях, поэтому я принимаю ответ, спасибо. Было бы гораздо проще, если бы кто-нибудь где-нибудь написал большую оговорку о том, что XSalsa20-Poly1305 из libsodium — это не то же самое, что XSalsa20 + Poly1305. Действительно, рецепт, что делать, надо расшифровать из статьи Бернштейна Cryptography in NaCl
. ИМХО, она должна даже получить отдельное имя как схема AEAD.
Я не понимаю этот код... Почему вы передаете пустой массив движку salsa для создания ключа Mac? Разве не для этого нужен
Poly1305KeyGenerator
? Это какой-то трюк по оптимизации производительности?