Статьи

Выбор криптографических алгоритмов Java, часть 2 — симметричное шифрование с одним ключом

Аннотация

Это вторая из серии статей из трех частей, посвященных криптографическим алгоритмам Java. В серии рассказывается, как реализовать следующее:

  1. Хеширование с помощью SHA – 512
  2. AES-256
  3. RSA-4096

Во втором посте подробно описано, как реализовать симметричное шифрование AES-256 с одним ключом. Давайте начнем.

отказ

Этот пост является исключительно информативным. Критически подумайте, прежде чем использовать любую представленную информацию. Учитесь на этом, но в конечном итоге принимайте свои собственные решения на свой страх и риск.

Требования

Я сделал всю работу для этого поста, используя следующие основные технологии. Вы можете сделать то же самое с разными технологиями или версиями, но без гарантий.

Скачать

Посетите мою страницу GitHub, чтобы увидеть все мои проекты с открытым исходным кодом. Код для этого поста находится в проекте: thoth-cryptography

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

Около

Алгоритмы симметричного шифрования основаны на одном ключе. Этот один ключ используется как для шифрования, так и для дешифрования. Таким образом, симметричные алгоритмы должны использоваться только при наличии строгого контроля для защиты ключа.

Симметричные алгоритмы обычно используются для шифрования и дешифрования данных в защищенных средах. Хорошим примером этого является защита связи Microservice. Если архитектура OAuth-2 / JWT выходит за рамки, API-шлюз может использовать один ключ симметричного алгоритма для шифрования токена. Этот токен затем передается другим микросервисам. Другие микросервисы используют тот же ключ для расшифровки токена. Другим хорошим примером являются гиперссылки, встроенные в электронные письма. Гиперссылки в электронных письмах содержат закодированный токен, который позволяет автоматически обрабатывать запрос на вход в систему при нажатии на гиперссылку. Этот токен является сильно зашифрованным значением, сгенерированным симметричным алгоритмом, поэтому его можно декодировать только на сервере приложений. И, конечно же, в любое время пароли или учетные данные любого типа должны быть защищены, для их шифрования используется симметричный алгоритм, и байты могут впоследствии расшифровываться с помощью того же ключа.

Исследования, проведенные на сегодняшний день, указывают на то, что наилучшим и наиболее безопасным алгоритмом шифрования с одним ключом, симметричным, является следующий (Sheth, 2017, «Выбор правильного алгоритма», пункт 2):

  1. Алгоритм: AES
  2. Режим: GCM
  3. Обивка : PKCS5Padding
  4. Размер ключа: 256 бит
  5. Размер IV: 96 бит

AES-256 использует 256-битный ключ, который требует установки пакета Unlimited Strength Java Cryptography Extension (JCE) . Давайте посмотрим на пример.

ПРИМЕЧАНИЕ. Для 256-битных ключей требуется пакет Unlimited Strength Java Cryptography Extension (JCE) . Если он не установлен, 128-битные ключи являются макс.

пример

Если у вас его еще нет, загрузите и установите пакет Unlimited Strength Java Cryptography Extension (JCE) . Требуется использовать 256-битные ключи. В противном случае приведенный ниже пример необходимо обновить, чтобы использовать 128-битный ключ.

В листинге 1 приведен модульный тест AesTest.java . Это полная демонстрация следующего:

  1. Создать и сохранить 256-битный ключ AES
  2. Шифрование AES
  3. Расшифровка AES

В листинге 2 показан AesSecretKeyProducer.java . Это вспомогательный класс, который отвечает за создание нового ключа или воспроизведение существующего ключа из byte[] .

В листинге 3 показан ByteArrayWriter.java, а в листинге 4 — ByteArrayReader.java . Это вспомогательные классы, отвечающие за чтение и запись byte[] в файл. Вы должны определить, как хранить byte[] вашего ключа, но его необходимо надежно хранить где-то (файл, база данных, репозиторий git и т. Д.).

Наконец, в листинге 5 показан Aes.java . Это вспомогательный класс, который отвечает за шифрование и дешифрование.

Листинг 1 — класс AesTest.java

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package org.thoth.crypto.symmetric;
 
import java.io.ByteArrayOutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import javax.crypto.SecretKey;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.thoth.crypto.io.ByteArrayReader;
import org.thoth.crypto.io.ByteArrayWriter;
 
/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesTest {
 
    static Path secretKeyFile;
 
    @BeforeClass
    public static void beforeClass() throws Exception {
        // Store the SecretKey bytes in the ./target diretory. Do
        // this so it will be ignore by source control.  We don't
        // want this file committed.
        secretKeyFile
            = Paths.get("./target/Aes256.key").toAbsolutePath();
 
        // Generate a SecretKey for the test
        SecretKey secretKey
            = new AesSecretKeyProducer().produce();
 
        // Store the byte[] of the SecretKey.  This is the
        // "private key file" you want to keep safe.
        ByteArrayWriter writer = new ByteArrayWriter(secretKeyFile);
        writer.write(secretKey.getEncoded());
    }
 
 
    @Test
    public void encrypt_and_decrypt_using_same_Aes256_instance() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );
 
        Aes aes
            = new Aes(secretKey);
 
        String toEncrypt
            = "encrypt me";
 
        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.empty());
 
        String decrypted
            = aes.decrypt(encryptedBytes, Optional.empty());
 
        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
 
 
    public void encrypt_and_decrypt_with_aad_using_same_Aes256_instance() {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );
 
        Aes aes
            = new Aes(secretKey);
 
        String toEncrypt
            = "encrypt me aad";
 
        // run
        byte[] encryptedBytes
            = aes.encrypt(toEncrypt, Optional.of("JUnit AAD"));
 
        String decrypted
            = aes.decrypt(encryptedBytes, Optional.of("JUnit AAD"));
 
        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
 
 
    @Test
    public void encrypt_and_decrypt_using_different_Aes256_instance()
    throws Exception {
        // setup
        SecretKey secretKey
            = new AesSecretKeyProducer().produce(
                new ByteArrayReader(secretKeyFile).read()
            );
 
        Aes aesForEncrypt
            = new Aes(secretKey);
 
        Aes aesForDecrypt
            = new Aes(secretKey);
 
        String toEncrypt
            = "encrypt me";
 
        // run
        byte[] encryptedBytes
            = aesForEncrypt.encrypt(toEncrypt, Optional.empty());
 
        ByteArrayOutputStream baos
            = new ByteArrayOutputStream();
        baos.write(encryptedBytes);
 
        String decrypted
            = aesForDecrypt.decrypt(baos.toByteArray(), Optional.empty());
 
        // assert
        Assert.assertEquals(toEncrypt, decrypted);
    }
}

Листинг 2 — класс AesSecretKeyProducer.java

01
02
03
04
05
06
07
08
09
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
package org.thoth.crypto.symmetric;
 
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
 
/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class AesSecretKeyProducer {
 
    /**
     * Generates a new AES-256 bit {@code SecretKey}.
     *
     * @return {@code SecretKey}, never null
     * @throws RuntimeException All exceptions are caught and re-thrown as {@code RuntimeException}
     */
    public SecretKey produce() {
        KeyGenerator keyGen;
        try {
            keyGen = KeyGenerator.getInstance("AES");
            keyGen.init(256);
            SecretKey secretKey = keyGen.generateKey();
            return secretKey;
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
 
 
    /**
     * Generates an AES-256 bit {@code SecretKey}.
     *
     * @param encodedByteArray The bytes this method will use to regenerate a previously created {@code SecretKey}
     *
     * @return {@code SecretKey}, never null
     * @throws RuntimeException All exceptions are caught and re-thrown as {@code RuntimeException}
     */
    public SecretKey produce(byte [] encodedByteArray) {
        try {
            return new SecretKeySpec(encodedByteArray, "AES");
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }
}

Листинг 3 — Класс ByteArrayWriter.java

01
02
03
04
05
06
07
08
09
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
package org.thoth.crypto.io;
 
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
 
/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class ByteArrayWriter {
 
    protected Path outputFile;
 
    private void initOutputFile(Path outputFile) {
        this.outputFile = outputFile;
    }
 
    private void initOutputDirectory() {
        Path outputDirectory = outputFile.getParent();
        if (!Files.exists(outputDirectory)) {
            try {
                Files.createDirectories(outputDirectory);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
 
    public ByteArrayWriter(Path outputFile) {
        initOutputFile(outputFile);
        initOutputDirectory();
    }
 
    public void write(byte[] bytesArrayToWrite) {
        try (
            OutputStream os
                = Files.newOutputStream(outputFile);
 
            PrintWriter writer
                new PrintWriter(os);
        ){
            for (int i=0; i<bytesArrayToWrite.length; i++) {
                if (i>0) {
                    writer.println();
                }
                writer.print(bytesArrayToWrite[i]);
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

Листинг 4 — Класс ByteArrayReader.java

01
02
03
04
05
06
07
08
09
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
package org.thoth.crypto.io;
 
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Scanner;
 
/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class ByteArrayReader {
 
    protected Path inputFile;
 
    public ByteArrayReader(Path inputFile) {
        this.inputFile = inputFile;
    }
 
    public byte[] read() {
        try (
            Scanner scanner
                new Scanner(inputFile);
 
            ByteArrayOutputStream baos
                = new ByteArrayOutputStream();
        ){
            while (scanner.hasNext()) {
                baos.write(Byte.parseByte(scanner.nextLine()));
            }
             
            baos.flush();
            return baos.toByteArray();
 
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
}

Листинг 5 — Класс Aes.java

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package org.thoth.crypto.symmetric;
 
import java.security.SecureRandom;
import java.util.Optional;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
 
/**
 *
 * @author Michael Remijan mjremijan@yahoo.com @mjremijan
 */
public class Aes {
 
    // If you don't have the Java Cryptography Extension
    // (JCE) Unlimited Strength packaged installed, use
    // a 128 bit KEY_SIZE.
    public static int KEY_SIZE = 256;
     
    public static int IV_SIZE = 12; // 12bytes * 8 = 96bits
    public static int TAG_BIT_SIZE = 128;
    public static String ALGORITHM_NAME = "AES";
    public static String MODE_OF_OPERATION = "GCM";
    public static String PADDING_SCHEME = "PKCS5Padding";
 
    protected SecretKey secretKey;
    protected SecureRandom secureRandom;
 
    public Aes(SecretKey secretKey) {
        this.secretKey = secretKey;
        this.secureRandom = new SecureRandom();
    }
 
 
    public byte[] encrypt(String message, Optional<String> aad) {
        try {
            // Transformation specifies algortihm, mode of operation and padding
            Cipher c = Cipher.getInstance(
                String.format("%s/%s/%s",ALGORITHM_NAME,MODE_OF_OPERATION,PADDING_SCHEME)
            );
 
            // Generate IV
            byte iv[] = new byte[IV_SIZE];
            secureRandom.nextBytes(iv); // SecureRandom initialized using self-seeding
 
            // Initialize GCM Parameters
            GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_SIZE, iv);
 
            // Init for encryption
            c.init(Cipher.ENCRYPT_MODE, secretKey, spec, secureRandom);
 
            // Add AAD tag data if present
            aad.ifPresent(t -> {
                try {
                    c.updateAAD(t.getBytes("UTF-8"));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
 
            // Add message to encrypt
            c.update(message.getBytes("UTF-8"));
 
            // Encrypt
            byte[] encryptedBytes
                = c.doFinal();
 
 
            // Concatinate IV and encrypted bytes.  The IV is needed later
            // in order to to decrypt.  The IV value does not need to be
            // kept secret, so it's OK to encode it in the return value
            //
            // Create a new byte[] the combined length of IV and encryptedBytes
            byte[] ivPlusEncryptedBytes = new byte[iv.length + encryptedBytes.length];
            // Copy IV bytes into the new array
            System.arraycopy(iv, 0, ivPlusEncryptedBytes, 0, iv.length);
            // Copy encryptedBytes into the new array
            System.arraycopy(encryptedBytes, 0, ivPlusEncryptedBytes, iv.length, encryptedBytes.length);
 
            // Return
            return ivPlusEncryptedBytes;
 
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
 
 
    public String decrypt(byte[] ivPlusEncryptedBytes, Optional<String> aad) {
 
        try {
            // Get IV
            byte iv[] = new byte[IV_SIZE];
            System.arraycopy(ivPlusEncryptedBytes, 0, iv, 0, IV_SIZE);
 
            // Initialize GCM Parameters
            GCMParameterSpec spec = new GCMParameterSpec(TAG_BIT_SIZE, iv);
 
            // Transformation specifies algortihm, mode of operation and padding
            Cipher c = Cipher.getInstance(
                String.format("%s/%s/%s",ALGORITHM_NAME,MODE_OF_OPERATION,PADDING_SCHEME)
            );
 
            // Get encrypted bytes
            byte [] encryptedBytes = new byte[ivPlusEncryptedBytes.length - IV_SIZE];
            System.arraycopy(ivPlusEncryptedBytes, IV_SIZE, encryptedBytes, 0, encryptedBytes.length);
 
            // Init for decryption
            c.init(Cipher.DECRYPT_MODE, secretKey, spec, secureRandom);
 
            // Add AAD tag data if present
            aad.ifPresent(t -> {
                try {
                    c.updateAAD(t.getBytes("UTF-8"));
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            });
 
            // Add message to decrypt
            c.update(encryptedBytes);
 
            // Decrypt
            byte[] decryptedBytes
                = c.doFinal();
 
            // Return
            return new String(decryptedBytes, "UTF-8");
 
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Резюме

Шифрование не легко. И простые примеры приведут к реализации с уязвимостями безопасности для вашего приложения. Если вам нужен единый ключ, симметричный алгоритм шифрования, используйте шифр AES / GCM / PKCS5Padding с 256-битным ключом и 96-битным IV.

Рекомендации

Опубликовано на Java Code Geeks с разрешения Майкла Ремиджана, партнера нашей программы JCG. См. Оригинальную статью здесь: Выбор алгоритмов шифрования Java. Часть 2. Симметричное шифрование с одним ключом.

Мнения, высказанные участниками Java Code Geeks, являются их собственными.