Java Crypto AES / GCM / NoPadding работает на Windows, но не на Docker (AEADBadTagException) - PullRequest
0 голосов
/ 07 февраля 2020

Я реализовал простой простой Java класс утилит для шифрования и дешифрования с использованием AES / GCM / NoPadding. Я использую этот фрагмент кода:

public byte[] encrypt(byte[] input, byte[] key, byte[] iv) throws Exception{
        Cipher cipher = initAES256GCMCipher(key, iv, Cipher.ENCRYPT_MODE);
        return cipher.doFinal(input);
}

public byte[] decrypt(byte[] input, byte[] key, byte[] iv) throws Exception{
        Cipher cipher = initAES256GCMCipher(key, iv, Cipher.DECRYPT_MODE);
        return cipher.doFinal(input);
}

private Cipher initAES256GCMCipher(byte[] key, byte[] iv, int encryptionMode) throws Exception{
        GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, iv);

        SecretKeySpec secretKey = new SecretKeySpec(key, "AES");

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(encryptionMode, secretKey, gcmParameterSpec);
        return cipher;
}

IV - это всегда 12-байтовый массив, ключ - это 32-байтовый массив , сгенерированный с помощью SecureRandom, принимающего начальное число . Я знаю, что на разных ОС SecureRandom отличается, но шифрование и дешифрование выполняются на одной и той же ОС, поэтому проблем быть не должно.

Это достаточно линейно, верно? Он отлично работает на Windows, шифрование и дешифрование возвращают один и тот же текст. Однако на изображении Docker тот же JAR не работает: шифрование работает нормально, но расшифровка выдает «AEADBadTagException».

Не могли бы вы мне помочь, пожалуйста?

1 Ответ

2 голосов
/ 07 февраля 2020

Не используйте SecureRandom для получения ключа. Для этого вы должны использовать функцию получения ключа (KDF), например HKDF . Но, честно говоря, вам нужно подумать о способе передачи ключа - без с использованием SecureRandom.

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

Несколько реализаций SecureRandom полностью определяют способ, которым возвращаются случайные биты, даже если задан базовый алгоритм (с DRBG). Обычно это не проблема, если вы ожидаете случайных значений. Однако, если вы используете его в качестве детерминированного c алгоритма, такого как KDF (или, например, функция ha sh), это становится проблемой, и вы можете получить разные ключи для одного и того же ввода.

В настоящее время вы должны иметь возможность хранить секретные ключи AES в хранилище ключей. Не уверен, что это решение для вашего варианта использования, но оно должно решить вашу текущую проблему. К сожалению, Java не содержит никаких официальных KDF, кроме PBKDF1 и PBKDF2, которые принимают пароль, а не ключ. Просто использование HMA C -SHA256 над некоторыми данными идентификации ключей с использованием мастер-ключа - это, как правило, хороший «KDF для бедных».


Вот быстрая, первоначальная (но недокументированная) реализация, которую я только что создан. Он имитирует Java JCA, фактически не вдаваясь в реализацию (так как без указания c Класс KDF для него не определен, но ).

Вы можете запросить много ключей с любым спецификация алгоритма, если его размер меньше 256 бит (вывод SHA-256) и метка отличается для каждого запрашиваемого ключа. Позвоните по номеру getEncoded(), если вам нужны данные, например, для IV.

import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import hex.Hex;

public class PoorMansKDF {

    public interface KeyDerivationParameters extends AlgorithmParameterSpec {
        String getDerivedKeyAlgorithm();
        int getDerivedKeySize();
        byte[] getCanonicalInfo();
    }

    public static class KeyDerivationParametersWithLabel implements KeyDerivationParameters {

        private final String algorithm;
        private final int keySize; 
        private final String label;

        public KeyDerivationParametersWithLabel(String algorithm, int keySize, String label) {
            this.algorithm = algorithm;
            this.keySize = keySize;
            this.label = label;
        }

        @Override
        public byte[] getCanonicalInfo() {
            if (label == null) {
                // array without elements
                return new byte[0];
            }
            return label.getBytes(StandardCharsets.UTF_8);
        }

        @Override
        public String getDerivedKeyAlgorithm() {
            return algorithm;
        }

        @Override
        public int getDerivedKeySize() {
            return keySize;
        }
    }

    private enum State {
        CONFIGURED,
        INITIALIZED;
    }

    public static PoorMansKDF getInstance() throws NoSuchAlgorithmException {
        return new PoorMansKDF();
    }

    private final Mac hmac;
    private State state;

    private PoorMansKDF() throws NoSuchAlgorithmException {
        this.hmac = Mac.getInstance("HMACSHA256");
        this.state = State.CONFIGURED;
    }

    public void init(Key key) throws InvalidKeyException {
        if (key.getAlgorithm().equalsIgnoreCase("HMAC")) {
            key = new SecretKeySpec(key.getEncoded(), "HMAC"); 
        }

        hmac.init(key);
        this.state = State.INITIALIZED;
    }

    public Key deriveKey(KeyDerivationParameters info) {
        if (state != State.INITIALIZED) {
            throw new IllegalStateException("Not initialized");
        }

        final int keySize = info.getDerivedKeySize();
        if (keySize < 0 || keySize % Byte.SIZE != 0 || keySize > hmac.getMacLength() * Byte.SIZE) {
            throw new IllegalArgumentException("Required key incompatible with this KDF");
        }
        final int keySizeBytes = keySize / Byte.SIZE;

        // we'll directly encode the info to bytes
        byte[] infoData = info.getCanonicalInfo();
        final byte[] fullHMAC = hmac.doFinal(infoData);
        final byte[] derivedKeyData;
        if (fullHMAC.length == keySizeBytes) {
            derivedKeyData = fullHMAC;
        } else {
            derivedKeyData = Arrays.copyOf(fullHMAC, keySizeBytes);
        }

        SecretKey derivedKey = new SecretKeySpec(derivedKeyData, info.getDerivedKeyAlgorithm());
        Arrays.fill(derivedKeyData, (byte) 0x00);
        if (fullHMAC != derivedKeyData) {
            Arrays.fill(fullHMAC, (byte) 0x00);
        }
        return derivedKey;
    }

    // test only
    public static void main(String[] args) throws Exception {
        var kdf = PoorMansKDF.getInstance();
        // input key (zero byte key for testing purposes, use your own 16-32 byte key)
        // do not use a password here!
        var masterKey = new SecretKeySpec(new byte[32], "HMAC");
        kdf.init(masterKey);

        // here "enc" is a label, in this case for a derived key for ENCryption 
        var labeledParameters = new KeyDerivationParametersWithLabel("AES", 256, "enc");
        var derivedKey = kdf.deriveKey(labeledParameters);
        // use your own hex decoder, e.g. from Apache Commons
        System.out.println(Hex.encode(derivedKey.getEncoded()));

        var aesCipher = Cipher.getInstance("AES/GCM/NoPadding");
        var gcmParams = new GCMParameterSpec(128, new byte[12]);
        aesCipher.init(Cipher.ENCRYPT_MODE, derivedKey, gcmParams);
        var ct = aesCipher.doFinal();
        System.out.println(Hex.encode(ct));
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...