首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >具有完整性和真实性的普通Java加密

具有完整性和真实性的普通Java加密
EN

Code Review用户
提问于 2018-02-23 17:08:39
回答 1查看 426关注 0票数 4

我花了相当长的时间编写了一个类,用于用密码对明文字节进行加密/解密。目标是除了java.*javax.*之外没有任何进一步的依赖关系。

它将按照千字节的顺序为加密的明文提供机密性和完整性,并且在选定的明文攻击(IND)(除了前导恒定字节之外)下是不可区分的。

代码语言:javascript
运行
复制
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;
    }
}

为什么不使用一个库呢?

  • Bouncy城堡是一个对简单加密的巨大依赖
  • Jasypt可能有一个简单的API,但我甚至找不到它在所选的明文攻击下是否提供了完整性或不可分辨性。

为什么不从堆栈溢出复制一些东西呢?

  • 我在Stackoverlow上找到的所有与加密相关的代码要么完全没有完整性,要么以某种方式被破坏,例如不使用KDF作为密码,不检查IV的完整性,.

编辑

我修复了一些小问题,您可以在这里找到整个源代码:https://github.com/trichner/tcrypt

EN

回答 1

Code Review用户

发布于 2018-05-11 13:05:10

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(); } }

在我看来这不像是在一起。如果代码应该对版本长度的变化具有鲁棒性,为什么序列(a)检查ciphertext.length对于版本不够长;(b)检查版本;(c)检查没有版本的密文是否足够长,以满足当前版本在块大小、标记长度等方面施加的额外限制?

考虑到最后一次检查强制VERSION_BYTE成为ciphertext的前缀,为什么对多字节标志的明智检查是对VERSION_BYTE的静态检查,而不是对ciphertext的实例检查?

// decipher and check MAC return cipher.doFinal(ciphertext, 13, ciphertext.length - GCM\_IV\_BYTES\_LENGTH - VERSION\_BYTE.length); // encrypt and MAC plaintext byte[] ciphertext = cipher.doFinal(plaintext);

声明的目标是处理“按千字节顺序加密的明文”。取决于约束有多紧,这里可能有问题。最近,我了解到一些提供商(没有明显的文档)限制了他们在doFinal中正确处理的密文或明文的大小。

\* The tag is calculated over the version byte, the IV as well as the ciphertext. // add IV to MAC cipher.updateAAD(VERSION\_BYTE); cipher.updateAAD(iv);

为什么?我几乎可以理解包括版本,虽然改变这可能会破坏事情。但是IV已经被MAC隐式地验证了,而不包括在AAD中。

我并不是说包含它是错误的,但是这是令人惊讶的,因此原因应该被记录下来,即使它只是一个指向crypto.stackexchange.com答案的链接,这就说明了包含它的理由。

byte[] ciphertext = cipher.doFinal(plaintext); // prepend VERSION and IV to ciphertext byte[] encrypted = new byte[1 + GCM\_IV\_BYTES\_LENGTH + ciphertext.length]; ... System.arraycopy(ciphertext, 0, encrypted, pos, ciphertext.length);

我很确定cipher.doFinal会超载,这样就可以节省那么大的System.arraycopy了。

票数 3
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/188206

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档