Афоризм
Как жаль, я Вам теперь не по карману.
Наталья Резник
Последние статьи

 • RawContacts
Необработанные контакты в Android
 • javax.crypto.Cypher
Cимметричное шифрование и дешифрирование
 • Random, Math.random
Генерация случайных чисел
 • Компонент JDatePicker
Описание и пример компонента JDatePicker
 • Компонент Tree
Описание и пример дерева Tree библиотеки base-gui
 • Grid с навигатором
Описание и пример Gridp с навигатором
 • Компонент Grid
Описание и пример Grid библиотеки base-gui
 • Библиотека base-gui
Описание компонентов библиотеки base-gui
 • Оператор SELECT
Использование SQL-оператора SELECT

Симметричное шифрование

Симметричные криптосистемы (симметричное шифрование) — это способ шифрования, в котором один и тот же криптографический ключ применяется для шифрования и дешифровывания. При асимметричном шифровании криптографические ключи шифрования и дешифрирования отличаются; шифрование осуществляется с помощью открытого ключа, а дешифрование с помощью закрытого ключа.

Определение шифра

Рассмотрим реализацию симметричного шифрования в java на примере алгоритма AES. В первую очередь нам понадобится определяющий алгоритм шифрования класс javax.crypto.Cipher, реализующий базовые функции популярных криптографических алгоритомов. Для получения экземпляра данного класса используется статистический метод Cipher.getInstance(), которому в качестве параметра передается наименование криптографического алгоритма шифрования. В простейшем случае пример создания экземпляра Java Cipher мог бы выглядеть следующим образом :

Cipher cipher = Cipher.getInstance("AES");

Однако, согласно документации JCA (Java Cryptography Architecture), в разделе "Creating a Cipher Object" указано, что для того, чтобы получить экземпляр Cipher нужно указать не просто алгоритм шифрования, а «трансформацию». Формат описания «трансформации» выглядит следующим образом: "algorithm/mode/padding" :

  • algorithm – наименование алгоритма согласно Cipher (Encryption) Algorithms (в примере используем AES);
  • mode – режим шифрования; например, ECB или CBC (представлены ниже);
  • padding – отступ/разбивка.

Параметр «padding» определяет, какой объём данных составляет 1 блок. Каждый блок данных шифруется отдельно. Так, например, padding, равный PKCS5Padding, определяет размер одного блока в 2 байта (16 бит).

Режимы шифрования

Режим шифрования определяет детали шифрования данных, которые косвенно влияют на алгоритм шифрования. Режимы шифрования могут использоваться в нескольких различных алгоритмах шифрования как метод, добавляемый к основному алгоритму. Поэтому режимы шифрования рассматриваются не отдельно от самих алгоритмов шифрования, а скорее как их «дополнения». Наиболее известные режимы шифрования :

ECBElectronic Codebook (режим электронной кодовой книги)
CBCCipher Block Chaining (режим сцепления блоков шифротекста)
CFBCipher Feedback (режим обратной связи по шифротексту)
OFBOutput Feedback (режим обратной связи по выходу)
CTRCounter (режим счетчика)

Отдельные алгоритмы шифрования могут работать в разных режимах.

В примере, рассмотренном ниже, при создании экземпляра Cipher будем использовать трансформацию "AES/ECB/PKCS5Padding". То есть, алгоритм шифрования – AES, режим шифрования – ECB, размер блока – 2 байта (PKCS5Padding).

Инициализация шифра

Прежде чем использовать Cipher для шифрования или дешифрирования текста, его необходимо инициализировать. Инициализация Cipher выполняется вызовом его метода init(int opmode, Key key), принимающего два параметра :

  • режим opmode, имеющий одно из значений Cipher.ENCRYPT_MODE или Cipher.DECRYPT_MODE;
  • секретный ключ типа java.security.Key.

Следующий код формирует экземпляр cipher для шифрования текста. Для дешифрирования текста необходимо определить режим Cipher.DECRYPT_MODE.

Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");

// Экземпляр cipher для шифрования текста
cipher.init(Cipher.ENCRYPT_MODE, secretKey);

Создание секретного ключа

Рассмотрим метод создания секретного ключа createSecretKey(), листинг которого представлен ниже. В методе сначала формируется массив bytes с использованием генератора псевдослучайныйх значений SecureRandom. После этого для массива bytes создается дайджест сообщения MessageDigest. Размер дайджеста составляет 20 байт, из которых выделяются первые 16 байт методом копирования в новый массив (copyOf) утилитой Arrays. Экземпляр секретного ключа SecretKeySpec создается для алгоритма "AES". Закомментированный в методе код позволяет вывести в консоль сгенерированный массив псевдослучайных значений.

private SecretKeySpec createSecretKey()
{
    SecretKeySpec sks    = null;        
    byte[]        bytes  = new byte[16];
    SecureRandom  random = new SecureRandom();
    random.nextBytes(bytes);
        
//  System.out.println("random : " + 
//          DatatypeConverter.printHexBinary(bytes));
    try {
        MessageDigest md;
        byte[]        key;
        md = MessageDigest.getInstance("SHA-1");

        key = md.digest(bytes);
        key = Arrays.copyOf(key, 16);
        sks = new SecretKeySpec(key, "AES");
    } catch (NoSuchAlgorithmException e) { }    	
    return sks;
}

Ключ алгоритма симметричного шифрования должен храниться в тайне обеими участниками. Алгоритм шифрования выбирается сторонами до начала обмена криптованными сообщениями. В методе createSecretKey таким ключом является переменная key. Ключ используется для создания SecretKeySpec, который необходим для инициализации Cipher.

Шифрование и дешифрирование

Для шифрования и дешифрования данных с помощью экземпляра Cipher, используется один из методов update() или doFinal(). Класс Cipher имеет несколько переопределенных методов update() и doFinal(), которые принимают разные параметры. Следующий пример демонстрирует использование метода doFinal() для шифрования текста :

byte[] plainText  = "Hello, World!".getBytes("UTF-8");
byte[] cipherText = cipher.doFinal(plainText);

System.out.println("encrypted text : " + 
            DatatypeConverter.printHexBinary(cipherText));

Для дешифрирования текст будет выглядеть примерно также, за исключением того, что экземпляр Cipher должен быть инициализирован соответствующим образом :

byte[] plainText = cipher.doFinal(cipherText);

Листинг примера CryptoExample

В примере CryptoExample представлены 2 метода : encrypt и decrypt. Исходный код метода createSecretKey рассмотрен выше. Алгоритм функционирования данных методов понятен из представленного выше описания. Дополнительно в методах encrypt и decrypt представлен код шифрования и дешифрирования с использованием утилиты java.util.Base64, которая появилась в Java 8.

Base64 — стандарт кодирования двоичных данных при помощи только 64 символов ASCII. Алфавит кодирования содержит текстово-цифровые латинские символы A-Z, a-z и 0-9 (62 знака) и 2 дополнительных символа, зависящих от системы реализации. Т.е. весь диапазон закодированных символов укладывается в английский алфавит, цифры и двух спецсимволов. Таким образом, из каждых 3 исходных байтов утилита Base64 формирует 4 символа, увеличивая размер текста на &#x2153.

В примере для криптования использован текст «decrypt!decrypt!decrypt!decrypt!», представляющий 4 повторяющихся размером 8 символов/байт слов «decrypt!». Т.е., кодируемый текст из 32 байт включает 2 повторяющихся выражения «decrypt!decrypt!» по 16 байт. А, как мы помним, режим PKCS5Padding определяет размер кодируемого блока в 2 байта.

public class CryptoExample
{
    Cipher cipher;
    String transformation = "AES/ECB/PKCS5Padding";

    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public CryptoExample() {}
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public byte[] encrypt(SecretKeySpec secretKey, 
                           byte[] plainText) {
        try {
            cipher = Cipher.getInstance(transformation);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            return cipher.doFinal(plainText);
//          return Base64.getEncoder()
//                       .encode(cipher.doFinal(plainText));
        } catch (Exception e) { }
        return null;
    }    
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public byte[] decrypt(SecretKeySpec secretKey, 
                          byte[] encryptedText) {
        try {
            cipher = Cipher.getInstance(transformation);
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
            return cipher.doFinal(encryptedText);
//          return cipher.doFinal(Base64.getDecoder()
//                                  .decode(encryptedText));
        } catch (Exception e) {}
        return null;
    }
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    private SecretKeySpec createSecretKey() {
        // исходный код представлен выше
		. . . 
    }
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    public static void main(String[] args) throws Exception 
    {
        CryptoExample cryptoEx = new CryptoExample();

        SecretKeySpec key = cryptoEx.createSecretKey();
        String text = "decrypt!decrypt!decrypt!decrypt!";
        
        byte[] enc = cryptoEx.encrypt(key, text.getBytes());
        System.out.println("Original text: '" + text + "'");
        System.out.println("Encrypted text :\n" + 
                    DatatypeConverter.printHexBinary(enc));

        byte[] bytes = cryptoEx.decrypt(key, enc);
        if (bytes != null) {
            String plainAfter = new String(bytes);
            System.out.println("Text after decryption: '" +
                                        plainAfter + "'");
        }
    }
}

Ну, и теперь смотрим на результат выполнения примера. В консоль были выведены следующие сообщения :

random : 992DE79A44047FCD641EC2F9F1A2056D

Original text: 'decrypt!decrypt!decrypt!decrypt!'

Encrypted text : 
CF2949AFAA65F0E2E8926364D56FBBFA \
CF2949AFAA65F0E2E8926364D56FBBFA \
6C1647F049941345172F0083EF997201

Text after decryption: 'decrypt!decrypt!decrypt!decrypt!'
 

Кодированный текст (Encrypted text) для наглядности, разделен на 3 части. После первых 2-х частей из 32 символов вставлены символы пробела и переносом '\', далее остальная часть кодированного текста.

В принципе, кодированный текст был восстановлен правильно. Но вот сам кодированный текст имеет 2 повторения (блоков также 2; остался неприятный осадок). Как это можно исправить? Можно использовать утилиту Base64, которая преобразует исходный текст и повторений не будет. Для этого можно снять комментарии в методах encrypt, decrypt. Но мы должны быть уверены, что у пользователя используется Java 8. Проще изменить режим шифрования – CBC (Cipher Block Chaining).

Режим шифрования CBC вводит понятие Initialization Vector, представленный классом IvParameterSpec. В данном режиме результат генерации предыдущего блока используется для генерации следующего.

Но чтобы использовать режим шифрования CBC требуется несложная доработка кода, связанная с инициализацией шифра Cipher. Для этого определяем массив из 16 байт sec_bytes, которые будем заполнять псевдослучайными значениями в конструкторе класса. При нициализации шифра будем использовать этот массив для формирования IvParameterSpec. Ниже представлены изменения кода примера для использования режима CBC.

// String transformation = "AES/ECB/PKCS5Padding";
   String transformation = "AES/CBC/PKCS5Padding";
   
    byte[] sec_bytes = new byte[16];

. . .
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public CryptoUtil() {
    SecureRandom random = new SecureRandom();
    random.nextBytes(sec_bytes);
}
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public byte[] encrypt(SecretKeySpec secretKey, 
                      byte[] plainText) {
    try {
        cipher = Cipher.getInstance(transformation);
        if (transformation.contains("ECB"))
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        else {
            IvParameterSpec ivSpec;
            ivSpec = new IvParameterSpec(sec_bytes);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, 
                                             ivSpec);
        }
//      return Base64.getEncoder()
//                   .encode(cipher.doFinal(plainText));
        return cipher.doFinal(plainText);
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }
    return null;
}    
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public byte[] decrypt(SecretKeySpec secretKey, 
                      byte[] encryptedText) {
    try {
        cipher = Cipher.getInstance(transformation);
        if (transformation.contains("ECB"))
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
        else {
            IvParameterSpec ivSpec;
            ivSpec = new IvParameterSpec(sec_bytes);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, 
                                             ivSpec);
        }
//      return cipher.doFinal(Base64.getDecoder()
//                                  .decode(encryptedText));
        return cipher.doFinal(encryptedText);
    } catch (Exception e) {
        System.out.println(e.getMessage());
    }
    return null;
}

После доработки повторения в кодированном тексте отсутствуют, результат шифрования и дешифрирования радует.

random : 5B1E804043C2068B9602285172800C3E

Original text: 'decrypt!decrypt!decrypt!decrypt!'

Encrypted text : 
F0802F50C5E41CFEE3F1F5EA71224771 \
8F1BCDF65519472EDCD6DE11FB0E14A1 \
D3A493CF59D7F2BB197CA90025E05203

Text after decryption: 'decrypt!decrypt!decrypt!decrypt!'
 

Полагаю, что данная статья помогла Вам решить задачу симметричного шифрования и дешифрирования. Ну, а вопрос передачи секретного ключа каждый решает самостоятельно.

  Рейтинг@Mail.ru