我花了相当长的时间编写了一个类,用于用密码对明文字节进行加密/解密。目标是除了java.*
和javax.*
之外没有任何进一步的依赖关系。
它将按照千字节的顺序为加密的明文提供机密性和完整性,并且在选定的明文攻击(IND)(除了前导恒定字节之外)下是不可区分的。
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;
}
}
我修复了一些小问题,您可以在这里找到整个源代码:https://github.com/trichner/tcrypt
发布于 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
了。
https://codereview.stackexchange.com/questions/188206
复制相似问题