Обычный шифрование Java с целостности и подлинности


Я потратил совсем немного времени и написал отдельный класс для шифрования/дешифрования текста байт кодовой фразой. Цель состоит в том, чтобы не иметь каких-либо дополнительных зависимостей, кроме java.* и javax.*.

Он обеспечивает конфиденциальность и целостность зашифрованном виде килобайтами и должны быть неотличимы по выбранным открытым текстом атака (Инд-ВМС) (кроме ведущего постоянно байт).

package cryptor;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * @author trichner
 * @created 19.02.18
 */
public class AesGcmCryptor {

    // https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
    private static final byte[] VERSION_BYTE = new byte[] { (byte) 0x01 };
    private static final int AES_KEY_BITS_LENGTH = 128;
    private static final int GCM_IV_BYTES_LENGTH = 12;
    private static final int GCM_TAG_BYTES_LENGTH = 16;

    private static final int PBKDF2_ITERATIONS = 16384;

    private static final byte[] PBKDF2_SALT = hexStringToByteArray("4d3fe0d71d2abd2828e7a3196ea450d4");

    /**
     * Decrypts an AES-GCM encrypted ciphertext and is
     * the reverse operation of {@link AesGcmCryptor#encrypt(char[], byte[])}
     *
     * @param password   passphrase for decryption
     * @param ciphertext encrypted bytes
     *
     * @return plaintext bytes
     *
     * @throws NoSuchPaddingException
     * @throws NoSuchAlgorithmException
     * @throws NoSuchProviderException
     * @throws InvalidKeySpecException
     * @throws InvalidAlgorithmParameterException
     * @throws InvalidKeyException
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws IllegalArgumentException           if the length or format of the ciphertext is bad
     */
    public byte[] decrypt(char[] password, byte[] ciphertext)
            throws NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException,
            InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
            BadVersionException {

        // input validation
        if (ciphertext == null) {
            throw new IllegalArgumentException("Ciphertext cannot be null.");
        }

        if (ciphertext.length <= VERSION_BYTE.length + GCM_IV_BYTES_LENGTH + GCM_TAG_BYTES_LENGTH) {
            throw new IllegalArgumentException("Ciphertext too short.");
        }

        // The version byte must have a 0 MSB in this version,
        // this allows us to expand the header to multiple bytes if ever necessary.
        // The MSB indicates if the current octet is the last octet of the header.
        if ((ciphertext[0] & (1 << 7)) != 0) {
            throw new BadVersionException();
        }

        // The version must match.
        for (int i = 0; i < VERSION_BYTE.length; i++) {
            if (VERSION_BYTE[i] != ciphertext[i]) {
                throw new BadVersionException();
            }
        }

        // input seems legit, lets decrypt and check integrity

        // derive key from password
        SecretKey key = deriveAesKey(password, PBKDF2_SALT, AES_KEY_BITS_LENGTH);

        // init cipher
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
        GCMParameterSpec params = new GCMParameterSpec(GCM_TAG_BYTES_LENGTH * 8,
                ciphertext,
                VERSION_BYTE.length,
                GCM_IV_BYTES_LENGTH
        );
        cipher.init(Cipher.DECRYPT_MODE, key, params);

        // add version and IV to MAC
        cipher.updateAAD(ciphertext, 0, GCM_IV_BYTES_LENGTH + VERSION_BYTE.length);

        // decipher and check MAC
        return cipher.doFinal(ciphertext, 13, ciphertext.length - GCM_IV_BYTES_LENGTH - VERSION_BYTE.length);
    }

    /**
     * Encrypts a plaintext with a password.
     *
     * The encryption provides the following security properties:
     * Confidentiality + Integrity
     *
     * This is achieved my using the AES-GCM AEAD blockmode with a randomized IV.
     *
     * The tag is calculated over the version byte, the IV as well as the ciphertext.
     *
     * Finally the encrypted bytes have the following structure:
     * <pre>
     *          +-------------------------------------------------------------------+
     *          |         |               |                             |           |
     *          | version | IV bytes      | ciphertext bytes            |    tag    |
     *          |         |               |                             |           |
     *          +-------------------------------------------------------------------+
     * Length:     1B        12B            len(plaintext) bytes            16B
     * </pre>
     * Note: There is no padding required for AES-GCM, but this also implies that
     * the exact plaintext length is revealed.
     *
     * @param password  password to use for encryption
     * @param plaintext plaintext to encrypt
     *
     * @throws NoSuchAlgorithmException
     * @throws NoSuchProviderException
     * @throws NoSuchPaddingException
     * @throws InvalidAlgorithmParameterException
     * @throws InvalidKeyException
     * @throws BadPaddingException
     * @throws IllegalBlockSizeException
     * @throws InvalidKeySpecException
     */
    public byte[] encrypt(char[] password, byte[] plaintext)
            throws NoSuchAlgorithmException, NoSuchProviderException, NoSuchPaddingException,
            InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
            InvalidKeySpecException {

        // initialise random and generate IV (initialisation vector)
        SecretKey key = deriveAesKey(password, PBKDF2_SALT, AES_KEY_BITS_LENGTH);
        final byte[] iv = new byte[GCM_IV_BYTES_LENGTH];
        SecureRandom random = SecureRandom.getInstanceStrong();
        random.nextBytes(iv);

        // encrypt
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "SunJCE");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_BYTES_LENGTH * 8, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, spec);

        // add IV to MAC
        cipher.updateAAD(VERSION_BYTE);
        cipher.updateAAD(iv);

        // encrypt and MAC plaintext
        byte[] ciphertext = cipher.doFinal(plaintext);

        // prepend VERSION and IV to ciphertext
        byte[] encrypted = new byte[1 + GCM_IV_BYTES_LENGTH + ciphertext.length];
        int pos = 0;
        System.arraycopy(VERSION_BYTE, 0, encrypted, 0, VERSION_BYTE.length);
        pos += VERSION_BYTE.length;
        System.arraycopy(iv, 0, encrypted, pos, iv.length);
        pos += iv.length;
        System.arraycopy(ciphertext, 0, encrypted, pos, ciphertext.length);

        return encrypted;
    }

    /**
     * We derive a fixed length AES key with uniform entropy from a provided
     * passphrase. This is done with PBKDF2/HMAC256 with a fixed count
     * of iterations and a provided salt.
     *
     * @param password passphrase to derive key from
     * @param salt     salt for PBKDF2 if possible use a per-key salt, alternatively
     *                 a random constant salt is better than no salt.
     * @param keyLen   number of key bits to output
     *
     * @return a SecretKey for AES derived from a passphrase
     *
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeySpecException
     */
    private SecretKey deriveAesKey(char[] password, byte[] salt, int keyLen)
            throws NoSuchAlgorithmException, InvalidKeySpecException {

        if (password == null || salt == null || keyLen <= 0) {
            throw new IllegalArgumentException();
        }
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        KeySpec spec = new PBEKeySpec(password, salt, PBKDF2_ITERATIONS, keyLen);
        SecretKey pbeKey = factory.generateSecret(spec);

        return new SecretKeySpec(pbeKey.getEncoded(), "AES");
    }

    /**
     * Helper to convert hex strings to bytes.
     *
     * This is neither null save nor does it go well with invalid hex strings.
     * Therefore it is important that this method is not used with user provided strings.
     */
    private static byte[] hexStringToByteArray(String s) {

        int len = s.length();

        byte[] data = new byte[len / 2];
        for (int i = 0; i < len - 1; i++) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16));
        }
        return data;
    }
}

Почему бы не использовать библиотеку?

  • Надувной замок-это огромная зависимость для простого шифрования
  • Jasypt может есть простой API, но я даже не мог узнать, если она обеспечивает целостность или неразличимость по выбранным открытым текстом атака

Почему не что-то копировать из StackOverflow?

  • Все шифрование кода я нашел на Stackoverlow либо нет целостности вообще, либо была нарушена тем или иным образом, например, не с помощью КДФ пароль, не проверив целостность ИЖ, ...

Редактировать

Я исправил некоторые незначительные проблемы, вы можете найти источник здесь: https://github.com/trichner/tcrypt



342
4
задан 23 февраля 2018 в 05:02 Источник Поделиться
Комментарии