Статья по безопасности Java

Предотвращение инъекций на Java

В этом разделе — практические советы по предотвращению инъекций в коде Java-приложений.

Пример кода, используемого в подсказках, находится здесь.

Что такое инъекция

Инъекция в OWASP Top 10 определяется следующим образом:

Рассмотрим всех, кто может отправлять в систему ненадежные данные, включая внешних пользователей, внутренних пользователей и администраторов.

Общие советы по предотвращению инъекций

Следующий пункт можно применить, в общем, для предотвращения Инъекция проблема:

  1. Применять Проверка ввода(с использованием списка разрешенных) в сочетании с Дезинфекция вывода + экранирование при пользовательском вводе/выводе.
  2. Если вам нужно взаимодействовать с системой, попробуйте использовать функции API, предоставляемые вашим стеком технологий (Java/.Net/PHP...), вместо команды сборки.

По этому поводу даются дополнительные консультации статья.

Конкретные типы инъекций

Примеры в этом разделе будут представлены в технологии Java (см. связанный проект Maven), но советы применимы и к другим технологиям, таким как .Net/PHP/Ruby/Python...

SQL

Симптом

Внедрение этого типа происходит, когда приложение использует ненадежный пользовательский ввод для построения SQL-запроса с использованием строки и его выполнения.

Как предотвратить

Использовать Параметризация запроса во избежание инъекций.

Пример

/*No DB framework used here in order to show the real use of
  Prepared Statement from Java API*/
/*Open connection with H2 database and use it*/
Class.forName("org.h2.Driver");
String jdbcUrl = "jdbc:h2:file:" + new File(".").getAbsolutePath() + "/target/db";
try (Connection con = DriverManager.getConnection(jdbcUrl)) {

    /* Sample A: Select data using Prepared Statement*/
    String query = "select * from color where friendly_name = ?";
    List<String> colors = new ArrayList<>();
    try (PreparedStatement pStatement = con.prepareStatement(query)) {
        pStatement.setString(1, "yellow");
        try (ResultSet rSet = pStatement.executeQuery()) {
            while (rSet.next()) {
                colors.add(rSet.getString(1));
            }
        }
    }

    /* Sample B: Insert data using Prepared Statement*/
    query = "insert into color(friendly_name, red, green, blue) values(?, ?, ?, ?)";
    int insertedRecordCount;
    try (PreparedStatement pStatement = con.prepareStatement(query)) {
        pStatement.setString(1, "orange");
        pStatement.setInt(2, 239);
        pStatement.setInt(3, 125);
        pStatement.setInt(4, 11);
        insertedRecordCount = pStatement.executeUpdate();
    }

   /* Sample C: Update data using Prepared Statement*/
    query = "update color set blue = ? where friendly_name = ?";
    int updatedRecordCount;
    try (PreparedStatement pStatement = con.prepareStatement(query)) {
        pStatement.setInt(1, 10);
        pStatement.setString(2, "orange");
        updatedRecordCount = pStatement.executeUpdate();
    }

   /* Sample D: Delete data using Prepared Statement*/
    query = "delete from color where friendly_name = ?";
    int deletedRecordCount;
    try (PreparedStatement pStatement = con.prepareStatement(query)) {
        pStatement.setString(1, "orange");
        deletedRecordCount = pStatement.executeUpdate();
    }

}

Ссылки

JPA

Симптом

Внедрение этого типа происходит, когда приложение использует ненадежный пользовательский ввод для построения запроса JPA с использованием строки и его выполнения. Это очень похоже на SQL-инъекцию, но здесь измененным языком является не SQL, а JPA QL.

Как предотвратить

Используйте язык запросов сохраняемости Java Параметризация запроса во избежание инъекций.

Пример

EntityManager entityManager = null;
try {
    /* Get a ref on EntityManager to access DB */
    entityManager = Persistence.createEntityManagerFactory("testJPA").createEntityManager();

    /* Define parameterized query prototype using named parameter to enhance readability */
    String queryPrototype = "select c from Color c where c.friendlyName = :colorName";

    /* Create the query, set the named parameter and execute the query */
    Query queryObject = entityManager.createQuery(queryPrototype);
    Color c = (Color) queryObject.setParameter("colorName", "yellow").getSingleResult();

} finally {
    if (entityManager != null && entityManager.isOpen()) {
        entityManager.close();
    }
}

Ссылки

Операционная система

Симптом

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

Как предотвратить

Используйте стек технологийAPI во избежание инъекций.

Пример

/* The context taken is, for example, to perform a PING against a computer.
* The prevention is to use the feature provided by the Java API instead of building
* a system command as String and execute it */
InetAddress host = InetAddress.getByName("localhost");
var reachable = host.isReachable(5000);

Ссылки

XML: XPath-инъекция

Симптом

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

Как предотвратить

Использовать Преобразователь переменных XPath во избежание инъекций.

Пример

Переменный резольвер выполнение.

/**
 * Resolver in order to define parameter for XPATH expression.
 *
 */
public class SimpleVariableResolver implements XPathVariableResolver {

    private final Map<QName, Object> vars = new HashMap<QName, Object>();

    /**
     * External methods to add parameter
     *
     * @param name Parameter name
     * @param value Parameter value
     */
    public void addVariable(QName name, Object value) {
        vars.put(name, value);
    }

    /**
     * {@inheritDoc}
     *
     * @see javax.xml.xpath.XPathVariableResolver#resolveVariable(javax.xml.namespace.QName)
     */
    public Object resolveVariable(QName variableName) {
        return vars.get(variableName);
    }
}

Код, использующий его для выполнения запроса XPath.

/*Create a XML document builder factory*/
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

/*Disable External Entity resolution for different cases*/
//Do not performed here in order to focus on variable resolver code
//but do it for production code !

/*Load XML file*/
DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(new File("src/test/resources/SampleXPath.xml"));

/* Create and configure parameter resolver */
String bid = "bk102";
SimpleVariableResolver variableResolver = new SimpleVariableResolver();
variableResolver.addVariable(new QName("bookId"), bid);

/*Create and configure XPATH expression*/
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setXPathVariableResolver(variableResolver);
XPathExpression xPathExpression = xpath.compile("//book[@id=$bookId]");

/* Apply expression on XML document */
Object nodes = xPathExpression.evaluate(doc, XPathConstants.NODESET);
NodeList nodesList = (NodeList) nodes;
Element book = (Element)nodesList.item(0);
var containsRalls = book.getTextContent().contains("Ralls, Kim");

Ссылки

HTML/JavaScript/CSS

Симптом

Внедрение этого типа происходит, когда приложение использует ненадежный пользовательский ввод для создания HTTP-ответа и отправляет его в браузер.

Как предотвратить

Либо примените строгую проверку ввода (подход со списком разрешенных), либо используйте очистку вывода + экранирование, если проверка ввода невозможна (объединяйте оба варианта каждый раз, когда это возможно).

Пример

/*
INPUT WAY: Receive data from user
Here it's recommended to use strict input validation using allowlist approach.
In fact, you ensure that only allowed characters are part of the input received.
*/

String userInput = "You user login is owasp-user01";

/* First we check that the value contains only expected character*/
if (!Pattern.matches("[a-zA-Z0-9\\s\\-]{1,50}", userInput))
{
    return false;
}

/* If the first check pass then ensure that potential dangerous character
that we have allowed for business requirement are not used in a dangerous way.
For example here we have allowed the character '-', and, this can
be used in SQL injection so, we
ensure that this character is not used is a continuous form.
Use the API COMMONS LANG v3 to help in String analysis...
*/
If (0 != StringUtils.countMatches(userInput.replace(" ", ""), "--"))
{
    return false;
}

/*
OUTPUT WAY: Send data to user
Here we escape + sanitize any data sent to user
Use the OWASP Java HTML Sanitizer API to handle sanitizing
Use the OWASP Java Encoder API to handle HTML tag encoding (escaping)
*/

String outputToUser = "You <p>user login</p> is <strong>owasp-user01</strong>";
outputToUser += "<script>alert(22);</script><img src='#' onload='javascript:alert(23);'>";

/* Create a sanitizing policy that only allow tag '<p>' and '<strong>'*/
PolicyFactory policy = new HtmlPolicyBuilder().allowElements("p", "strong").toFactory();

/* Sanitize the output that will be sent to user*/
String safeOutput = policy.sanitize(outputToUser);

/* Encode HTML Tag*/
safeOutput = Encode.forHtml(safeOutput);
String finalSafeOutputExpected = "You <p>user login</p> is <strong>owasp-user01</strong>";
if (!finalSafeOutputExpected.equals(safeOutput))
{
    return false;
}

Ссылки

ЛДАП

посвященный статья был создан.

NoSQL

Симптом

Внедрение этого типа происходит, когда приложение использует ненадежный пользовательский ввод для построения выражения вызова API NoSQL.

Как предотвратить

Поскольку существует множество систем баз данных NoSQL, и каждая из них использует API для вызова, важно гарантировать, что пользовательский ввод, полученный и используемый для построения выражения вызова API, не содержит каких-либо символов, имеющих особое значение в синтаксисе целевого API. Это делается для того, чтобы избежать использования его для экранирования исходного выражения вызова для создания другого на основе созданного пользовательского ввода. Также важно не использовать конкатенацию строк для создания выражения вызова API, а использовать API для создания выражения.

Пример — МонгоБД

/* Here use MongoDB as target NoSQL DB */
String userInput = "Brooklyn";

/* First ensure that the input do no contains any special characters
for the current NoSQL DB call API,
here they are: ' " \ ; { } $
*/
//Avoid regexp this time in order to made validation code
//more easy to read and understand...
ArrayList < String > specialCharsList = new ArrayList < String > () {
    {
        add("'");
        add("\"");
        add("\\");
        add(";");
        add("{");
        add("}");
        add("$");
    }
};

for (String specChar: specialCharsList) {
    if (userInput.contains(specChar)) {
        return false;
    }
}

//Add also a check on input max size
if (!userInput.length() <= 50)
{
    return false;
}

/* Then perform query on database using API to build expression */
//Connect to the local MongoDB instance
try(MongoClient mongoClient = new MongoClient()){
    MongoDatabase db = mongoClient.getDatabase("test");
    //Use API query builder to create call expression
    //Create expression
    Bson expression = eq("borough", userInput);
    //Perform call
    FindIterable<org.bson.Document> restaurants = db.getCollection("restaurants").find(expression);
    //Verify result consistency
    restaurants.forEach(new Block<org.bson.Document>() {
        @Override
        public void apply(final org.bson.Document doc) {
            String restBorough = (String)doc.get("borough");
            if (!"Brooklyn".equals(restBorough))
            {
                return false;
            }
        }
    });
}

Ссылки

Внедрение журналов

Симптом

Внедрение журналов происходит, когда приложение включает ненадежные данные в сообщение журнала приложения (например, злоумышленник может создать дополнительную запись журнала, которая будет выглядеть так, как будто она поступила от совершенно другого пользователя, если он сможет ввести символы CRLF в ненадежные данные). Дополнительную информацию об этой атаке можно найти на сайте OWASP. Внедрение журналов страница.

Как предотвратить

Чтобы злоумышленник не смог записать вредоносный контент в журнал приложений, примените такие средства защиты, как:

  • Используйте структурированные форматы журналов, такие как JSON, вместо неструктурированных текстовых форматов. Неструктурированные форматы подвержены С свадьба Р возврат (CR) и л внутри Ф э.д. (LF) инъекция (см.CWE-93).
  • Ограничьте размер входного значения пользователя, используемого для создания сообщения журнала.
  • Убеждаться вся защита от XSS применяются при просмотре файлов журналов в веб-браузере.

Пример использования Log4j Core 2

Рекомендуемая политика ведения журналов для производственной среды — отправка журналов в сетевой сокет с использованием структурированного протокола. Макет шаблона JSON введено в Лог4j 2.14.0 и ограничьте размер строк до 500 байт, используя maxStringLength атрибут конфигурации:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration xmlns="https://logging.apache.org/xml/ns"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="
                   https://logging.apache.org/xml/ns
                   https://logging.apache.org/xml/ns/log4j-config-2.xsd">
  <Appenders>
    <Socket name="SOCKET"
            host="localhost"
            port="12345">
      <!-- Limit the size of any string field in the produced JSON document to 500 bytes -->
      <JsonTemplateLayout maxStringLength="500"
                          nullEventDelimiterEnabled="true"/>
    </Socket>
  </Appenders>
  <Loggers>
    <Root level="DEBUG">
      <AppenderRef ref="SOCKET"/>
    </Root>
  </Loggers>
</Configuration>

Видеть Интеграция с сервис-ориентированными архитектурами на Веб-сайт Log4j дополнительные советы.

Использование регистратора на уровне кода:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
...
// Most common way to declare a logger
private static final LOGGER = LogManager.getLogger();
// GOOD!
//
// Use parameterized logging to add user data to a message
// The pattern should be a compile-time constant
logger.warn("Login failed for user {}.", username);
// BAD!
//
// Don't mix string concatenation and parameters
// If `username` contains `{}`, the exception will leak into the message
logger.warn("Failure for user " + username + " and role {}.", role, ex);
...

Видеть Лучшие практики API Log4j для получения дополнительной информации.

Пример использования журнала

Рекомендуемая политика ведения журнала для производственной среды — использование структурированного JsonEncoder введено в Вход в систему 1.3.8. В приведенном ниже примере Logback настроен на чередование 10 файлов журналов по 5 МБ каждый:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration>
<configuration>
  <import class="ch.qos.logback.classic.encoder.JsonEncoder"/>
  <import class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy"/>
  <import class="ch.qos.logback.core.rolling.RollingFileAppender"/>
  <import class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"/>

  <appender name="RollingFile" class="RollingFileAppender">
    <file>app.log</file>
    <rollingPolicy class="FixedWindowRollingPolicy">
      <fileNamePattern>app-%i.log</fileNamePattern>
      <minIndex>1</minIndex>
      <maxIndex>10</maxIndex>
    </rollingPolicy>
    <triggeringPolicy class="SizeBasedTriggeringPolicy">
      <maxFileSize>5MB</maxFileSize>
    </triggeringPolicy>
    <encoder class="JsonEncoder"/>
  </appender>

  <root level="DEBUG">
    <appender-ref ref="SOCKET"/>
  </root>
</configuration>

Использование регистратора на уровне кода:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
// Most common way to declare a logger
Logger logger = LoggerFactory.getLogger(MyClass.class);
// GOOD!
//
// Use parameterized logging to add user data to a message
// The pattern should be a compile-time constant
logger.warn("Login failed for user {}.", username);
// BAD!
//
// Don't mix string concatenation and parameters
// If `username` contains `{}`, the exception will leak into the message
logger.warn("Failure for user " + username + " and role {}.", role, ex);
...

Ссылки

Криптография

Общее руководство по криптографии

  • Никогда, никогда не пишите свои собственные криптографические функции.
  • По возможности старайтесь вообще не писать какой-либо криптографический код. Вместо этого попробуйте использовать уже существующие решения по управлению секретами или решение по управлению секретами, предоставленное вашим облачным провайдером. Для получения дополнительной информации см. Статья по управлению секретами OWASP.
  • Если вы не можете использовать уже существующее решение для управления секретами, попробуйте использовать надежную и хорошо известную библиотеку реализации, а не использовать библиотеки, встроенные в JCA/JCE, поскольку с их помощью слишком легко допустить криптографические ошибки.
  • Убедитесь, что ваше приложение или протокол могут легко поддерживать будущие изменения криптографических алгоритмов.
  • По возможности используйте менеджер пакетов, чтобы поддерживать все ваши пакеты в актуальном состоянии. Следите за обновлениями в настройках разработки и соответствующим образом планируйте обновления своих приложений.
  • Ниже мы покажем примеры на основе Google Tink — библиотеки, созданной экспертами по криптографии для безопасного использования криптографии (в смысле минимизации распространенных ошибок, допускаемых при использовании стандартных библиотек криптографии).

Шифрование для хранения

Следуйте инструкциям по алгоритму в Статья по криптографическому хранилищу OWASP.

Симметричный пример с использованием Google Tink

В Google Tink есть документация по выполнению распространенных задач.

Например, на этой странице (с сайта Google) показано как выполнить простое симметричное шифрование.

В следующем фрагменте кода показано инкапсулированное использование этой функции:

Нажмите здесь, чтобы просмотреть фрагмент кода «Симметричное шифрование Tink».
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.crypto.tink.Aead;
import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
import com.google.crypto.tink.aead.AeadConfig;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

// AesGcmSimpleTest
public class App {

    // Based on example from:
    // https://github.com/tink-crypto/tink-java/tree/main/examples/aead

    public static void main(String[] args) throws Exception {

        // Key securely generated using:
        // tinkey create-keyset --key-template AES128_GCM --out-format JSON --out aead_test_keyset.json



        // Register all AEAD key types with the Tink runtime.
        AeadConfig.register();

        // Read the keyset into a KeysetHandle.
        KeysetHandle handle =
        TinkJsonProtoKeysetFormat.parseKeyset(
            new String(Files.readAllBytes( Paths.get("/home/fredbloggs/aead_test_keyset.json")), UTF_8), InsecureSecretKeyAccess.get());

        String message = "This message to be encrypted";
        System.out.println(message);

        // Add some relevant context about the encrypted data that should be verified
        // on decryption
        String metadata = "Sender: fredbloggs@example.com";

        // Encrypt the message
        byte[] cipherText = AesGcmSimple.encrypt(message, metadata, handle);
        System.out.println(Base64.getEncoder().encodeToString(cipherText));

        // Decrypt the message
        String message2 = AesGcmSimple.decrypt(cipherText, metadata, handle);
        System.out.println(message2);
    }
}

class AesGcmSimple {

    public static byte[] encrypt(String plaintext, String metadata, KeysetHandle handle) throws Exception {
        // Get the primitive.
        Aead aead = handle.getPrimitive(Aead.class);
        return aead.encrypt(plaintext.getBytes(UTF_8), metadata.getBytes(UTF_8));
    }

    public static String decrypt(byte[] ciphertext, String metadata, KeysetHandle handle) throws Exception {
        // Get the primitive.
        Aead aead = handle.getPrimitive(Aead.class);
        return new String(aead.decrypt(ciphertext, metadata.getBytes(UTF_8)),UTF_8);
    }

}

Симметричный пример с использованием встроенных классов JCA/JCE

Если вы абсолютно не можете использовать отдельную библиотеку, все равно можно использовать встроенные классы JCA/JCE, но настоятельно рекомендуется, чтобы специалист по криптографии проверил весь проект и код, поскольку даже самая тривиальная ошибка может серьезно ослабить ваше шифрование.

В следующем фрагменте кода показан пример использования AES-GCM для шифрования и дешифрования данных.

Несколько ограничений/подводных камней с этим кодом:

  • Он не учитывает ротацию ключей или управление, что само по себе является целой темой.
  • Важно использовать разные одноразовые номера для каждой операции шифрования, особенно если используется один и тот же ключ. Для получения дополнительной информации см. этот ответ на Cryptography Stack Exchange.
  • Ключ необходимо будет надежно хранить.
Нажмите здесь, чтобы просмотреть фрагмент кода «Симметричное шифрование JCA/JCE».
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.crypto.spec.*;
import javax.crypto.*;
import java.util.Base64;


// AesGcmSimpleTest
class Main {

    public static void main(String[] args) throws Exception {
        // Key of 32 bytes / 256 bits for AES
        KeyGenerator keyGen = KeyGenerator.getInstance(AesGcmSimple.ALGORITHM);
        keyGen.init(AesGcmSimple.KEY_SIZE, new SecureRandom());
        SecretKey secretKey = keyGen.generateKey();

        // Nonce of 12 bytes / 96 bits and this size should always be used.
        // It is critical for AES-GCM that a unique nonce is used for every cryptographic operation.
        byte[] nonce = new byte[AesGcmSimple.IV_LENGTH];
        SecureRandom random = new SecureRandom();
        random.nextBytes(nonce);

        var message = "This message to be encrypted";
        System.out.println(message);

        // Encrypt the message
        byte[] cipherText = AesGcmSimple.encrypt(message, nonce, secretKey);
        System.out.println(Base64.getEncoder().encodeToString(cipherText));

        // Decrypt the message
        var message2 = AesGcmSimple.decrypt(cipherText, nonce, secretKey);
        System.out.println(message2);
    }
}

class AesGcmSimple {

    public static final String ALGORITHM = "AES";
    public static final String CIPHER_ALGORITHM = "AES/GCM/NoPadding";
    public static final int KEY_SIZE = 256;
    public static final int TAG_LENGTH = 128;
    public static final int IV_LENGTH = 12;

    public static byte[] encrypt(String plaintext, byte[] nonce, SecretKey secretKey) throws Exception {
        return cryptoOperation(plaintext.getBytes(StandardCharsets.UTF_8), nonce, secretKey, Cipher.ENCRYPT_MODE);
    }

    public static String decrypt(byte[] ciphertext, byte[] nonce, SecretKey secretKey) throws Exception {
        return new String(cryptoOperation(ciphertext, nonce, secretKey, Cipher.DECRYPT_MODE), StandardCharsets.UTF_8);
    }

    private static byte[] cryptoOperation(byte[] text, byte[] nonce, SecretKey secretKey, int mode) throws Exception {
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(TAG_LENGTH, nonce);
        cipher.init(mode, secretKey, gcmParameterSpec);
        return cipher.doFinal(text);
    }

}

Шифрование для передачи

Опять же, следуйте инструкциям по алгоритму в Статья по криптографическому хранилищу OWASP.

Асимметричный пример с использованием Google Tink

В Google Tink есть документация по выполнению распространенных задач.

Например, на этой странице (с сайта Google) показано как выполнить процесс гибридного шифрования где две стороны хотят обмениваться данными на основе своей пары асимметричных ключей.

Следующий фрагмент кода показывает, как эту функцию можно использовать для обмена секретами между Алисой и Бобом:

Нажмите здесь, чтобы просмотреть фрагмент кода «Гибридное шифрование Tink».
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.crypto.tink.HybridDecrypt;
import com.google.crypto.tink.HybridEncrypt;
import com.google.crypto.tink.InsecureSecretKeyAccess;
import com.google.crypto.tink.KeysetHandle;
import com.google.crypto.tink.TinkJsonProtoKeysetFormat;
import com.google.crypto.tink.hybrid.HybridConfig;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;

// HybridReplaceTest
class App {
    public static void main(String[] args) throws Exception {
        /*

        Generated public/private keypairs for Bob and Alice using the
        following tinkey commands:

        ./tinkey create-keyset \
        --key-template DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM \
        --out-format JSON --out alice_private_keyset.json

        ./tinkey create-keyset \
        --key-template DHKEM_X25519_HKDF_SHA256_HKDF_SHA256_AES_256_GCM \
        --out-format JSON --out bob_private_keyset.json

        ./tinkey create-public-keyset --in alice_private_keyset.json \
        --in-format JSON --out-format JSON --out alice_public_keyset.json

        ./tinkey create-public-keyset --in bob_private_keyset.json \
        --in-format JSON --out-format JSON --out bob_public_keyset.json
        */

        HybridConfig.register();

        // Generate ECC key pair for Alice
        var alice = new HybridSimple(
                getKeysetHandle("/home/alicesmith/private_keyset.json"),
                getKeysetHandle("/home/alicesmith/public_keyset.json")

        );

        KeysetHandle alicePublicKey = alice.getPublicKey();

        // Generate ECC key pair for Bob
        var bob = new HybridSimple(
                getKeysetHandle("/home/bobjones/private_keyset.json"),
                getKeysetHandle("/home/bobjones/public_keyset.json")

        );

        KeysetHandle bobPublicKey = bob.getPublicKey();

        // This keypair generation should be reperformed every so often in order to
        // obtain a new shared secret to avoid a long lived shared secret.

        // Alice encrypts a message to send to Bob
        String plaintext = "Hello, Bob!";

        // Add some relevant context about the encrypted data that should be verified
        // on decryption
        String metadata = "Sender: alicesmith@example.com";

        System.out.println("Secret being sent from Alice to Bob: " + plaintext);
        var cipherText = alice.encrypt(bobPublicKey, plaintext, metadata);
        System.out.println("Ciphertext being sent from Alice to Bob: " + Base64.getEncoder().encodeToString(cipherText));


        // Bob decrypts the message
        var decrypted = bob.decrypt(cipherText, metadata);
        System.out.println("Secret received by Bob from Alice: " + decrypted);
        System.out.println();

        // Bob encrypts a message to send to Alice
        String plaintext2 = "Hello, Alice!";

        // Add some relevant context about the encrypted data that should be verified
        // on decryption
        String metadata2 = "Sender: bobjones@example.com";

        System.out.println("Secret being sent from Bob to Alice: " + plaintext2);
        var cipherText2 = bob.encrypt(alicePublicKey, plaintext2, metadata2);
        System.out.println("Ciphertext being sent from Bob to Alice: " + Base64.getEncoder().encodeToString(cipherText2));

        // Bob decrypts the message
        var decrypted2 = alice.decrypt(cipherText2, metadata2);
        System.out.println("Secret received by Alice from Bob: " + decrypted2);
    }

    private static KeysetHandle getKeysetHandle(String filename) throws Exception
    {
        return TinkJsonProtoKeysetFormat.parseKeyset(
                new String(Files.readAllBytes( Paths.get(filename)), UTF_8), InsecureSecretKeyAccess.get());
    }
}
class HybridSimple {

    private KeysetHandle privateKey;
    private KeysetHandle publicKey;


    public HybridSimple(KeysetHandle privateKeyIn, KeysetHandle publicKeyIn) throws Exception {
        privateKey = privateKeyIn;
        publicKey = publicKeyIn;
    }

    public KeysetHandle getPublicKey() {
        return publicKey;
    }

    public byte[] encrypt(KeysetHandle partnerPublicKey, String message, String metadata) throws Exception {

        HybridEncrypt encryptor = partnerPublicKey.getPrimitive(HybridEncrypt.class);

        // return the encrypted value
        return encryptor.encrypt(message.getBytes(UTF_8), metadata.getBytes(UTF_8));
    }
    public String decrypt(byte[] ciphertext, String metadata) throws Exception {

        HybridDecrypt decryptor = privateKey.getPrimitive(HybridDecrypt.class);

        // return the encrypted value
        return new String(decryptor.decrypt(ciphertext, metadata.getBytes(UTF_8)),UTF_8);
    }


}

Асимметричный пример с использованием встроенных классов JCA/JCE

Если вы абсолютно не можете использовать отдельную библиотеку, все равно можно использовать встроенные классы JCA/JCE, но настоятельно рекомендуется, чтобы специалист по криптографии проверил весь проект и код, поскольку даже самая тривиальная ошибка может серьезно ослабить ваше шифрование.

В следующем фрагменте кода показан пример использования эллиптической кривой/Диффи-Хельмана (ECDH) вместе с AES-GCM для шифрования/дешифрования данных между двумя разными сторонами без необходимости передачи симметричного ключа между двумя сторонами. Вместо этого стороны обмениваются открытыми ключами и затем могут использовать ECDH для создания общего секрета, который можно использовать для симметричного шифрования.

Обратите внимание, что этот пример кода использует класс AesGcmSimple из пакета предыдущий раздел.

Несколько ограничений/подводных камней с этим кодом:

  • Он не учитывает ротацию ключей или управление, что само по себе является целой темой.
  • Код намеренно применяет новый одноразовый номер для каждой операции шифрования, но его необходимо рассматривать как отдельный элемент данных рядом с зашифрованным текстом.
  • Приватные ключи необходимо будет надежно хранить.
  • Код не учитывает проверку открытых ключей перед использованием.
  • В целом, между двумя сторонами нет никакой проверки подлинности.
Нажмите здесь, чтобы просмотреть фрагмент кода «гибридного шифрования JCA/JCE».
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import javax.crypto.spec.*;
import javax.crypto.*;
import java.util.*;
import java.security.*;
import java.security.spec.*;
import java.util.Arrays;

// ECDHSimpleTest
class Main {
    public static void main(String[] args) throws Exception {

        // Generate ECC key pair for Alice
        var alice = new ECDHSimple();
        Key alicePublicKey = alice.getPublicKey();

        // Generate ECC key pair for Bob
        var bob = new ECDHSimple();
        Key bobPublicKey = bob.getPublicKey();

        // This keypair generation should be reperformed every so often in order to
        // obtain a new shared secret to avoid a long lived shared secret.

        // Alice encrypts a message to send to Bob
        String plaintext = "Hello"; //, Bob!";
        System.out.println("Secret being sent from Alice to Bob: " + plaintext);

        var retPair = alice.encrypt(bobPublicKey, plaintext);
        var nonce = retPair.getKey();
        var cipherText = retPair.getValue();

        System.out.println("Both cipherText and nonce being sent from Alice to Bob: " + Base64.getEncoder().encodeToString(cipherText) + " " + Base64.getEncoder().encodeToString(nonce));


        // Bob decrypts the message
        var decrypted = bob.decrypt(alicePublicKey, cipherText, nonce);
        System.out.println("Secret received by Bob from Alice: " + decrypted);
        System.out.println();

        // Bob encrypts a message to send to Alice
        String plaintext2 = "Hello"; //, Alice!";
        System.out.println("Secret being sent from Bob to Alice: " + plaintext2);

        var retPair2 = bob.encrypt(alicePublicKey, plaintext2);
        var nonce2 = retPair2.getKey();
        var cipherText2 = retPair2.getValue();
        System.out.println("Both cipherText2 and nonce2 being sent from Bob to Alice: " + Base64.getEncoder().encodeToString(cipherText2) + " " + Base64.getEncoder().encodeToString(nonce2));

        // Bob decrypts the message
        var decrypted2 = alice.decrypt(bobPublicKey, cipherText2, nonce2);
        System.out.println("Secret received by Alice from Bob: " + decrypted2);
    }
}
class ECDHSimple {
    private KeyPair keyPair;

    public class AesKeyNonce {
        public SecretKey Key;
        public byte[] Nonce;
    }

    public ECDHSimple() throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
        ECGenParameterSpec ecSpec = new ECGenParameterSpec("secp256r1"); // Using secp256r1 curve
        keyPairGenerator.initialize(ecSpec);
        keyPair = keyPairGenerator.generateKeyPair();
    }

    public Key getPublicKey() {
        return keyPair.getPublic();
    }

    public AbstractMap.SimpleEntry<byte[], byte[]> encrypt(Key partnerPublicKey, String message) throws Exception {

        // Generate the AES Key and Nonce
        AesKeyNonce aesParams = generateAESParams(partnerPublicKey);

        // return the encrypted value
        return new AbstractMap.SimpleEntry<>(
            aesParams.Nonce,
            AesGcmSimple.encrypt(message, aesParams.Nonce, aesParams.Key)
            );
    }
    public String decrypt(Key partnerPublicKey, byte[] ciphertext, byte[] nonce) throws Exception {

        // Generate the AES Key and Nonce
        AesKeyNonce aesParams = generateAESParams(partnerPublicKey, nonce);

        // return the decrypted value
        return AesGcmSimple.decrypt(ciphertext, aesParams.Nonce, aesParams.Key);
    }

    private AesKeyNonce generateAESParams(Key partnerPublicKey, byte[] nonce) throws Exception {

        // Derive the secret based on this side's private key and the other side's public key
        KeyAgreement keyAgreement = KeyAgreement.getInstance("ECDH");
        keyAgreement.init(keyPair.getPrivate());
        keyAgreement.doPhase(partnerPublicKey, true);
        byte[] secret = keyAgreement.generateSecret();

        AesKeyNonce aesKeyNonce = new AesKeyNonce();

        // Copy first 32 bytes as the key
        byte[] key = Arrays.copyOfRange(secret, 0, (AesGcmSimple.KEY_SIZE / 8));
        aesKeyNonce.Key = new SecretKeySpec(key, 0, key.length, "AES");

        // Passed in nonce will be used.
        aesKeyNonce.Nonce = nonce;
        return aesKeyNonce;

    }

    private AesKeyNonce generateAESParams(Key partnerPublicKey) throws Exception {

        // Nonce of 12 bytes / 96 bits and this size should always be used.
        // It is critical for AES-GCM that a unique nonce is used for every cryptographic operation.
        // Therefore this is not generated from the shared secret
        byte[] nonce = new byte[AesGcmSimple.IV_LENGTH];
        SecureRandom random = new SecureRandom();
        random.nextBytes(nonce);
        return generateAESParams(partnerPublicKey, nonce);

    }
}

© Перевод на русский язык. Оригинальные материалы: OWASP Cheat Sheet Series.
Этот проект использует материалы OWASP, распространяемые по лицензии CC BY-SA 4.0.