Привет, Хабр! Представляю вашему вниманию перевод второй статьи "Java Cipher" автора Jakob Jenkov из серии статей для начинающих, желающих освоить основы криптографии в Java.
Оглавление:
- Cryptography
- Cipher
- MessageDigest
- Mac
- Signature
- KeyPair
- KeyGenerator
- KeyPairGenerator
- KeyStore
- Keytool
- Certificate
- CertificateFactory
- CertPath
Java Cipher (Шифр)
Класс Java Cipher (javax.crypto.Cipher) представляет собой алгоритм шифрования. Термин «Шифр» является стандартным термином для алгоритма шифрования в мире криптографии. Вот почему класс Java называется Шифр(Cipher), а не Шифратор / Дешифратор или как-то еще. Вы можете использовать экземпляр Cipher для шифрования и расшифровки данных в Java. В этой главе объясняется, как работает класс Cipher.
Создание шифра
Прежде чем использовать шифр, необходимо создать экземпляр класса Cipher, вызывая его метод getInstance() с параметром, указывающим, какой тип алгоритма шифрования вы хотите использовать. Вот пример создания экземпляра Java Cipher:
Cipher cipher = Cipher.getInstance("AES");
В этом примере создается экземпляр Cipher с использованием алгоритма шифрования AES.
Режимы шифрования
Некоторые алгоритмы шифрования могут работать в разных режимах. Режим шифрования определяет детали того, как будут зашифрованы данные. Таким образом, режим шифрования частично влияет на алгоритм шифрования. Режимы шифрования могут иногда использоваться в нескольких различных алгоритмах шифрования — как метод, который добавляется к основному алгоритму шифрования. Вот почему режимы рассматриваются отдельно от самих алгоритмов шифрования, а скорее как «дополнения» к алгоритмам шифрования. Вот некоторые из наиболее известных режимов шифрования:
- EBC — Electronic Codebook (Режим электронной кодовой книги)
- CBC — Cipher Block Chaining (Режим сцепления блоков шифротекста)
- CFB — Cipher Feedback (Режим обратной связи по шифротексту)
- OFB — Output Feedback (Режим обратной связи по выходу)
- CTR — Counter (Режим счетчика)
При создании экземпляра шифра вы можете добавить его режим к имени алгоритма шифрования. Создать экземпляр AES Cipher с использованием режима сцепления блоков — Cipher Block Chaining (CBC), можно так:
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
Поскольку режим сцепления блоков шифрования также требует «схемы дополнения», схема дополнения (PKCS5Padding) добавляется в конец строки имени алгоритма шифрования.
Важно знать, что не все алгоритмы и режимы шифрования поддерживаются поставщиком шифрования Java SDK по умолчанию. Для создания нужного вам экземпляра шифра с требуемым режимом и схемой заполнения может понадобиться установить сторонний провайдер, такой как Bouncy Castle.
Инициализация шифра
Прежде чем использовать экземпляр Cipher, его необходимо инициализировать. Инициализация шифра выполняется путем вызова его метода init(). Метод init() принимает два параметра:
- Режим
- Ключ
Пример инициализации экземпляра Cipher в режиме шифрования:
Key key = ... // получить/создать симметричный ключ шифрования
cipher.init(Cipher.ENCRYPT_MODE, key);
А вот пример инициализации экземпляра Cipher уже в режиме расшифровки:
Key key = ... //получить/создать симметричный ключ шифрования
cipher.init(Cipher.DECRYPT_MODE, key);
Шифрование и расшифровка данных
Чтобы зашифровать или расшифровать данные с помощью экземпляра Cipher, вызывается один из этих двух методов:
update()
doFinal()
Существует несколько переопределенных версий методов update() и doFinal(), которые принимают разные параметры. Рассмотрим наиболее часто используемые здесь. Если вам необходимо зашифровать или расшифровать один блок данных, просто вызовите doFinal() с данными для шифрования или расшифровки. Пример:
byte[] plainText = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] cipherText = cipher.doFinal(plainText);
На самом деле код выглядит примерно так же и в случае расшифровки данных. Просто помните, что экземпляр Cipher должен быть инициализирован в режиме расшифровки. Вот как выглядит расшифровка одного блока зашифрованного текста:
byte[] plainText = cipher.doFinal(cipherText);
Если вам нужно зашифровать или расшифровать большой файл разбитый на несколько блоков, вызывается update() один раз для каждого блока данных и завершается вызовом метода doFinal() с последним блоком данных. Вот пример шифрования нескольких блоков данных:
byte[] data1 = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] data2 = "zyxwvutsrqponmlkjihgfedcba".getBytes("UTF-8");
byte[] data3 = "01234567890123456789012345".getBytes("UTF-8");
byte[] cipherText1 = cipher.update(data1);
byte[] cipherText2 = cipher.update(data2);
byte[] cipherText3 = cipher.doFinal(data3);
Причина, по которой вызов doFinal() необходим для последнего блока данных, заключается в том, что некоторые алгоритмы шифрования должны дополнять данные, чтобы соответствовать определенному размеру блока шифра (например, 8-байтовой границе). Дополнять же промежуточные зашифрованные данные нет необходимости. Следовательно, вызывается метод update() для промежуточных блоков данных и вызов doFinal() для последнего блока данных.
При расшифровке нескольких блоков данных вы также вызываете метод update() для промежуточных блоков данных и метод doFinal() для последнего блока. Пример расшифровки нескольких блоков данных:
byte[] plainText1 = cipher.update(cipherText1);
byte[] plainText2 = cipher.update(cipherText2);
byte[] plainText3 = cipher.doFinal(cipherText3);
Опять же, экземпляр шифра должен быть инициализирован в режиме дешифровки, чтобы этот пример работал.
Шифрование / Расшифровка части байтового массива
Методы шифрования и расшифровки класса Cipher могут шифровать или расшифровывать часть данных, хранящихся в байтовом массиве. Методу update() и/или doFinal() нужно передать смещение и длину.
int offset = 10;
int length = 24;
byte[] cipherText = cipher.doFinal(data, offset, length);
В данном примере будут зашифрованы (или расшифрованы, в зависимости от инициализации шифра) байты с 10 индекса и на 24 байта вперед.
Шифрование / Расшифровка в существующий байтовый массив
Все приведенные в этой главе примеры шифрования и дешифрования возвращают зашифрованные или дешифрованные данные в новом байтовом массиве. Однако также возможно зашифровать или расшифровать данные в существующий байтовый массив. Это может быть полезно для уменьшения количества созданных байтовых массивов. Для этого необходимо передать целевой массив байтов в качестве параметра методу update() и/или doFinal().
int offset = 10;
int length = 24;
byte[] dest = new byte[1024];
cipher.doFinal(data, offset, length, dest);
В этом примере данные шифруются с 10 индекса на 24 байта вперед в байтовый массив dest со смещением 0. Если вы хотите установить другое смещение для байтового массива dest, существуют версии update() и doFinal(), которые принимают дополнительный параметр смещения. Пример вызова метода doFinal() со смещением в массиве dest:
int offset = 10;
int length = 24;
byte[] dest = new byte[1024];
int destOffset = 12
cipher.doFinal(data, offset, length, dest, destOffset);
Повторное использование экземпляра шифра
Инициализация экземпляра Cipher — дорогостоящая операция и хорошей идеей будет повторное использование экземпляров Cipher. К счастью, класс Cipher был разработан с учетом возможности повторного использования. Когда вы вызываете метод doFinal() для экземпляра Cipher, он возвращается в состояние, в котором находился сразу после инициализации. Экземпляр Cipher может затем использоваться для шифрования или дешифрования большего количества данных.
Пример повторного использования экземпляра Java Cipher:
Cipher cipher = Cipher.getInstance("AES");
Key key = ... //получить/создать симметричный ключ шифрования
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] data1 = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
byte[] data2 = "zyxwvutsrqponmlkjihgfedcba".getBytes("UTF-8");
byte[] cipherText1 = cipher.update(data1);
byte[] cipherText2 = cipher.doFinal(data2);
byte[] data3 = "01234567890123456789012345".getBytes("UTF-8");
byte[] cipherText3 = cipher.doFinal(data3);
Сначала создается и инициализируется экземпляр Cipher, а затем используется для шифрования двух блоков согласованных данных. Обратите внимание на вызов update(), а затем doFinal() для этих двух блоков данных. После этого экземпляр Cipher может быть снова использован для шифрования данных. Это делается с помощью вызова doFinal() с третьим блоком данных. После этого вызова doFinal() вы можете зашифровать еще один блок данных с тем же экземпляром Java Cipher.