В этой статье будет попытка написать шифрование сообщения алгоритмом AES, а также в добавление к нему цифровой подписи о алгоритму SHA512.
Для начала создадим новый Maven проект и в файл “pom.xml” добавим зависимость:
1 2 3 4 5 |
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk16</artifactId> <version>1.46</version> </dependency |
Создадим экземпляр AESEngine:
1 |
new AESEngine() |
Пока всё просто. Но просто так использовать AES нельзя. Нам нужно ещё использовать режим сцепления блоков (Cipher Block Chaining), чтобы результат шифрования последующего блока зависел от результата шифрования предыдущего. Подобная тактика защитит от вставки злоумышленником дополнительных блоков в середине, а также затруднит поиск часто встречающихся фраз в зашифрованном тексте, что могло бы скомпрометировать все наши усилия. Cipher Block Chaining за нас будет реализовывать класс CBCBlockCipher:
1 |
new CBCBlockCipher(new AESEngine()) |
Уже лучше. Теперь нужно решить ещё одну проблему. AES и CBC оперируют блоками. Мы же передаём строки произвольной длины. Что делать, если размер передаваемой нами строки не может делиться на размер блока без остатка? Как передавать оставшийся хвост, который меньше размера блока? Для этого используется дополнение или padding, то есть дополнительные данные, вставляемые в конец сообщения. Мы будем использовать PKCS7 padding:
1 2 |
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); |
Также мы будем подписывать зашифрованные сообщения с помощью SHA512:
1 |
HMac hmac = new HMac(new SHA512Digest()); |
Алгоритмы AES и SHA512 используют ключи, которые мы будем передавать в наш код в массиве байт:
1 2 3 |
byte[] aesKey; byte[] hmacShaKey; |
Экземпляру HMac нужно указать ключ:
1 |
hmac.init(new KeyParameter(hmacShaKey)) |
Нам ещё нужен вектор инициализации (initialization vector или IV) — случайное число, которое вместе с ключом будет использоваться в алгоритме для шифрования и расшифровки. Каждое новое сообщение будет иметь другой IV, что позволит генерировать разные результаты шифрования даже для одинаковых частей сообщений. Мы же понимаем, что в передаваемых сообщениях могут повторяться, например, имена людей, приветствие или ещё что-либо. Благодаря IV шифрование будет порождать разные результаты для этих повторяющихся кусков текста в разных сообщениях:
1 2 3 |
byte[] iv = new byte[cipher.getBlockSize()]; SecureRandom random = new SecureRandom(); random.nextBytes(iv); |
Обратите внимание, что для генерации IV мы использовали не обычный Random, а SecureRandom. Обычный Random не подходит для генерации случайных чисел, связанных с криптографией.
Проинициализируем наш cipher, передав ему ключи и IV:
1 |
cipher.init(true, new ParametersWithIV(new KeyParameter(aesKey), iv)); |
IV не обязательно скрывать. Его можно передавать получателю в качестве первого блока шифрованного сообщения, причём в открытом виде, в IV нет ничего секретного:
1 |
outputStream.write(iv); |
Дальше всё просто. Мы вызываем cipher.processBytes и hmac.update, передавая им считанные из исходного сообщения блоки байт, а под конец вызываем cipher.doFinal вычисления и записи последнего блока, а также hmac.doFinal для вычисления подписи:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
byte[] buffer = new byte[BUFFER_SIZE]; byte[] outputBuffer = new byte[BUFFER_SIZE]; byte[] signBuffer = new byte[hmac.getMacSize()]; int readedBytes; while ((readedBytes = inputStream.read(buffer)) != -1) { int outputLen = cipher.processBytes(buffer, 0, readedBytes, outputBuffer, 0); outputStream.write(outputBuffer, 0, outputLen); hmac.update(outputBuffer, 0, outputLen); } try { int outputLen = cipher.doFinal(outputBuffer, 0); outputStream.write(outputBuffer, 0, outputLen); hmac.update(outputBuffer, 0, outputLen); hmac.doFinal(signBuffer, 0); outputStream.write(signBuffer, 0, hmac.getMacSize()); } catch (CryptoException ce) { throw new BcExampleException(ce); } |
И самое главное! Под конец заполняем нулями все используемые в процессе массивы байт, чтобы они в ОЗУ не оставалось приватной информации и ключей после использования (статья на эту тему):
1 2 3 4 |
Arrays.fill(iv, (byte) 0); Arrays.fill(buffer, (byte) 0); Arrays.fill(outputBuffer, (byte) 0); Arrays.fill(signBuffer, (byte) 0); |
Конечный результат:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public void encrypt(InputStream inputStream, OutputStream outputStream) throws BcExampleException, IOException { BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); HMac hmac = new HMac(new SHA512Digest()); hmac.init(new KeyParameter(hmacShaKey)); byte[] iv = new byte[cipher.getBlockSize()]; SecureRandom random = new SecureRandom(); random.nextBytes(iv); cipher.init(true, new ParametersWithIV(new KeyParameter(aesKey), iv)); outputStream.write(iv); byte[] buffer = new byte[BUFFER_SIZE]; byte[] outputBuffer = new byte[BUFFER_SIZE]; byte[] signBuffer = new byte[hmac.getMacSize()]; int readedBytes; while ((readedBytes = inputStream.read(buffer)) != -1) { int outputLen = cipher.processBytes( buffer, 0, readedBytes, outputBuffer, 0); outputStream.write(outputBuffer, 0, outputLen); hmac.update(outputBuffer, 0, outputLen); } try { int outputLen = cipher.doFinal(outputBuffer, 0); outputStream.write(outputBuffer, 0, outputLen); hmac.update(outputBuffer, 0, outputLen); hmac.doFinal(signBuffer, 0); outputStream.write(signBuffer, 0, hmac.getMacSize()); } catch (CryptoException ce) { throw new BcExampleException(ce); } Arrays.fill(iv, (byte) 0); Arrays.fill(buffer, (byte) 0); Arrays.fill(outputBuffer, (byte) 0); Arrays.fill(signBuffer, (byte) 0); } |
Отлично. С шифрованием разобрались. Теперь нам нужно реализовать расшифровку сообщения.
Инициализация cipher и hmac происходит аналогично:
1 2 3 4 5 6 |
BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); HMac hmac; hmac = new HMac(new SHA512Digest()); hmac.init(new KeyParameter(hmacShaKey)); |
Так как вектор инициализации мы записали в начале шифрованного сообщения, то считаем его обратно:
1 2 3 4 5 6 7 8 9 10 11 12 |
byte[] iv = new byte[cipher.getBlockSize()]; int readedBytes; int offset = 0; int limit = iv.length; while ((limit - offset > 0) && ((readedBytes = inputStream.read(iv, offset, limit)) != -1)) { offset += readedBytes; limit = iv.length - offset; } if (limit > 0) { throw new BcExampleException("Incorrect input stream length."); } |
Дальше просто вызываем cipher.processBytes и hmac.update, как и в случае с шифрованием:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
byte[] buffer = new byte[BUFFER_SIZE + hmac.getMacSize()]; byte[] outputBuffer = new byte[BUFFER_SIZE]; byte[] signBuffer = new byte[hmac.getMacSize()]; offset = 0; limit = buffer.length; int signOffset = 0; while ((readedBytes = inputStream.read(buffer, offset, limit)) != -1) { if (readedBytes > hmac.getMacSize()) { int processBytes = readedBytes - hmac.getMacSize(); int outputLen = cipher.processBytes(buffer, 0, processBytes, outputBuffer, 0); hmac.update(buffer, 0, processBytes); outputStream.write(outputBuffer, 0, outputLen); signOffset = processBytes; offset = 0; limit = buffer.length; } else { offset += readedBytes; limit = buffer.length - readedBytes; } } try { int outputLen = cipher.doFinal(outputBuffer, 0); outputStream.write(outputBuffer, 0, outputLen); } catch (CryptoException ce) { throw new BcExampleException(ce); } byte[] calculatedSign = new byte[hmac.getMacSize()]; byte[] hmacSign = Arrays.copyOfRange(buffer, signOffset, signOffset + hmac.getMacSize()); hmac.doFinal(calculatedSign, 0); |
Затем сравниваем полученные подписи:
1 2 3 |
if (!Arrays.equals(hmacSign, calculatedSign)) { throw new SignException(); } |
Очищаем массивы байт после использования:
1 2 3 4 5 |
Arrays.fill(buffer, (byte) 0); Arrays.fill(outputBuffer, (byte) 0); Arrays.fill(signBuffer, (byte) 0); Arrays.fill(calculatedSign, (byte) 0); Arrays.fill(hmacSign, (byte) 0); |
В результате должен получиться вот такой метод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
public void decrypt(InputStream inputStream, OutputStream outputStream) throws BcExampleException, IOException { BufferedBlockCipher cipher = new PaddedBufferedBlockCipher( new CBCBlockCipher(new AESEngine()), new PKCS7Padding()); HMac hmac; hmac = new HMac(new SHA512Digest()); hmac.init(new KeyParameter(hmacShaKey)); byte[] iv = new byte[cipher.getBlockSize()]; int readedBytes; int offset = 0; int limit = iv.length; while ((limit - offset > 0) && ((readedBytes = inputStream.read(iv, offset, limit)) != -1)) { offset += readedBytes; limit = iv.length - offset; } if (limit > 0) { throw new BcExampleException("Incorrect input stream length."); } cipher.init(false, new ParametersWithIV( new KeyParameter(aesKey), iv, 0, iv.length)); byte[] buffer = new byte[BUFFER_SIZE + hmac.getMacSize()]; byte[] outputBuffer = new byte[BUFFER_SIZE]; byte[] signBuffer = new byte[hmac.getMacSize()]; offset = 0; limit = buffer.length; int signOffset = 0; while ((readedBytes = inputStream.read(buffer, offset, limit)) != -1) { if (readedBytes > hmac.getMacSize()) { int processBytes = readedBytes - hmac.getMacSize(); int outputLen = cipher.processBytes( buffer, 0, processBytes, outputBuffer, 0); hmac.update(buffer, 0, processBytes); outputStream.write(outputBuffer, 0, outputLen); signOffset = processBytes; offset = 0; limit = buffer.length; } else { offset += readedBytes; limit = buffer.length - readedBytes; } } try { int outputLen = cipher.doFinal(outputBuffer, 0); outputStream.write(outputBuffer, 0, outputLen); } catch (CryptoException ce) { throw new BcExampleException(ce); } byte[] calculatedSign = new byte[hmac.getMacSize()]; byte[] hmacSign = Arrays.copyOfRange(buffer, signOffset, signOffset + hmac.getMacSize()); hmac.doFinal(calculatedSign, 0); if (!Arrays.equals(hmacSign, calculatedSign)) { throw new SignException(); } Arrays.fill(buffer, (byte) 0); Arrays.fill(outputBuffer, (byte) 0); Arrays.fill(signBuffer, (byte) 0); Arrays.fill(calculatedSign, (byte) 0); Arrays.fill(hmacSign, (byte) 0); } |
Вот и всё. Пример вызова наших методов:
1 2 3 4 5 6 7 8 9 10 11 |
KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); SecretKey secretKey = keyGenerator.generateKey(); byte[] aesKey = secretKey.getEncoded(); byte[] hmacShaKey = "hmacShaKey".getBytes(StandardCharsets.UTF_8); byte[] message = "myMessage".getBytes(StandardCharsets.UTF_8); Crypter crypter = new Crypter(aesKey, hmacShaKey); ByteArrayOutputStream encryptedStream = new ByteArrayOutputStream(); crypter.encrypt(new ByteArrayInputStream(message), encryptedStream); ByteArrayOutputStream decryptedStream = new ByteArrayOutputStream(); crypter.decrypt(new ByteArrayInputStream(encryptedStream.toByteArray()), decryptedStream); System.out.println("Decrypted: " + new String(decryptedStream.toByteArray(), StandardCharsets.UTF_8)); |