Здесь простое решение с только java.*
и javax.crypto.*
зависимостями для шифрования байтов, обеспечивающее конфиденциальность и целостность . Он должен быть неотличимым при выбранной атаке открытым текстом для коротких сообщений порядка килобайт.
Он использует AES
в режиме GCM
без заполнения, 128-битный ключ получается из PBKDF2
с большим количеством итераций и статической солью из предоставленного пароля. Это гарантирует, что перебор паролей будет трудным и распределяет энтропию по всему ключу.
Генерируется случайный вектор инициализации (IV), который будет добавлен к зашифрованному тексту. Кроме того, статический байт 0x01
добавляется как первый байт как «версия».
Все сообщение отправляется в код аутентификации сообщения (MAC), сгенерированный AES/GCM
.
Вот так, класс шифрования с нулевыми внешними зависимостями обеспечивает конфиденциальность и целостность :
<code>package ch.n1b.tcrypt.utils;
import java.nio.charset.StandardCharsets;
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.*;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
/**
* This class implements AES-GCM symmetric key encryption with a PBKDF2 derived password.
* It provides confidentiality and integrity of the plaintext.
*
* @author Thomas Richner
* @created 2018-12-07
*/
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 = 0x01;
private static final int VERSION_BYTE_LENGTH = 1;
private static final int AES_KEY_BITS_LENGTH = 128;
// fixed AES-GCM constants
private static final String GCM_CRYPTO_NAME = "AES/GCM/NoPadding";
private static final int GCM_IV_BYTES_LENGTH = 12;
private static final int GCM_TAG_BYTES_LENGTH = 16;
// can be tweaked, more iterations = more compute intensive to brute-force password
private static final int PBKDF2_ITERATIONS = 1024;
// protects against rainbow tables
private static final byte[] PBKDF2_SALT = hexStringToByteArray("4d3fe0d71d2abd2828e7a3196ea450d4");
public String encryptString(char[] password, String plaintext) throws CryptoException {
byte[] encrypted = null;
try {
encrypted = encrypt(password, plaintext.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException //
| InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException //
| InvalidKeySpecException e) {
throw new CryptoException(e);
}
return byteArrayToHexString(encrypted);
}
public String decryptString(char[] password, String ciphertext)
throws CryptoException {
byte[] ct = hexStringToByteArray(ciphertext);
byte[] plaintext = null;
try {
plaintext = decrypt(password, ct);
} catch (AEADBadTagException e) {
throw new CryptoException(e);
} catch ( //
NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeySpecException //
| InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException //
| BadPaddingException e) {
throw new CryptoException(e);
}
return new String(plaintext, StandardCharsets.UTF_8);
}
/**
* 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
* @throws CryptoException
*/
public byte[] decrypt(char[] password, byte[] ciphertext)
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException,
InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
// 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 must match, we don't decrypt other versions
if (ciphertext[0] != VERSION_BYTE) {
throw new IllegalArgumentException("wrong version: " + ciphertext[0]);
}
// 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(GCM_CRYPTO_NAME);
GCMParameterSpec params = new GCMParameterSpec(GCM_TAG_BYTES_LENGTH * 8,
ciphertext,
VERSION_BYTE_LENGTH,
GCM_IV_BYTES_LENGTH
);
cipher.init(Cipher.DECRYPT_MODE, key, params);
final int ciphertextOffset = VERSION_BYTE_LENGTH + GCM_IV_BYTES_LENGTH;
// add version and IV to MAC
cipher.updateAAD(ciphertext, 0, ciphertextOffset);
// decipher and check MAC
return cipher.doFinal(ciphertext, ciphertextOffset, ciphertext.length - ciphertextOffset);
}
/**
* Encrypts a plaintext with a password.
* <p>
* The encryption provides the following security properties:
* Confidentiality + Integrity
* <p>
* This is achieved my using the AES-GCM AEAD blockmode with a randomized IV.
* <p>
* The tag is calculated over the version byte, the IV as well as the ciphertext.
* <p>
* Finally the encrypted bytes have the following structure:
* <pre>
* +-------------------------------------------------------------------+
* | | | | |
* | version | IV bytes | ciphertext bytes | tag |
* | | | | |
* +-------------------------------------------------------------------+
* Length: 1B 12B len(plaintext) bytes 16B
*
* Примечание: для AES-GCM не требуется заполнение, но это также подразумевает, что
* указана точная длина открытого текста.
*
* @param пароль пароль, чтобы использовать для шифрования
* @param открытый текст для шифрования
* @throws NoSuchAlgorithmException
* @throws NoSuchProviderException
* @throws NoSuchPaddingException
* @throws InvalidAlgorithmParameterException
* @throws InvalidKeyException
* @throws BadPaddingException
* @throws IllegalBlockSizeException
* @throws InvalidKeySpecException
* /
публичный байт [] шифрование (char [] пароль, байт [] открытый текст)
выдает NoSuchAlgorithmException, NoSuchPaddingException,
InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
InvalidKeySpecException {
// инициализируем случайное число и генерируем IV (вектор инициализации)
Ключ SecretKey = производнаяAesKey (пароль, PBKDF2_SALT, AES_KEY_BITS_LENGTH);
последний байт [] iv = новый байт [GCM_IV_BYTES_LENGTH];
SecureRandom random = SecureRandom.getInstanceStrong ();
random.nextBytes (IV);
// зашифровать
Cipher cipher = Cipher.getInstance (GCM_CRYPTO_NAME);
GCMParameterSpec spec = новый GCMParameterSpec (GCM_TAG_BYTES_LENGTH * 8, iv);
cipher.init (Cipher.ENCRYPT_MODE, ключ, спецификация);
// добавляем IV в MAC
последний байт [] versionBytes = новый байт [] {VERSION_BYTE};
cipher.updateAAD (versionBytes);
cipher.updateAAD (IV);
// шифрование и открытый текст MAC
byte [] ciphertext = cipher.doFinal (открытый текст);
// добавляем VERION и IV к шифрованному тексту
byte [] encrypted = новый байт [1 + GCM_IV_BYTES_LENGTH + ciphertext.length];
int pos = 0;
System.arraycopy (versionBytes, 0, зашифрованный, 0, VERSION_BYTE_LENGTH);
pos + = VERSION_BYTE_LENGTH;
System.arraycopy (iv, 0, зашифрованный, pos, iv.length);
pos + = длина волны;
System.arraycopy (зашифрованный текст, 0, зашифрованный, pos, ciphertext.length);
возврат зашифрован;
}
/ **
* Мы получаем ключ AES фиксированной длины с равномерной энтропией из
* пароль. Это делается с PBKDF2 / HMAC256 с фиксированным счетом
* итераций и предоставленной соли.
*
* @param пароль пароль для получения ключа из
* соль соли @param для PBKDF2, если возможно, используйте соль для каждого ключа, или
* случайная постоянная соль лучше, чем без соли.
* @param keyLen количество битов ключа для вывода
* @ возврат секретного ключа для AES, полученного из парольной фразы
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* /
закрытое SecretKey производноеAesKey (пароль char [], соль байта [], int keyLen)
выдает NoSuchAlgorithmException, InvalidKeySpecException {
if (пароль == ноль || salt == ноль || keyLen <= 0) {
бросить новый IllegalArgumentException ();
}SecretKeyFactory factory = SecretKeyFactory.getInstance ("PBKDF2WithHmacSHA256");
KeySpec spec = новый PBEKeySpec (пароль, соль, PBKDF2_ITERATIONS, keyLen);
SecretKey pbeKey = factory.generateSecret (spec);
вернуть новый SecretKeySpec (pbeKey.getEncoded (), "AES");
}
/ **
* Помощник для преобразования шестнадцатеричных строк в байты.
* <p>
* Может использоваться для чтения байтов из констант.
* /
закрытый статический байт [] hexStringToByteArray (String s) {
if (s == null) {
throw new IllegalArgumentException ("Предоставленная строка` null`. ");
}
int len = s.length ();
if (len% 2! = 0) {
бросить новое IllegalArgumentException ("Недопустимая длина:" + len);
}
byte [] data = new byte [len / 2];
для (int i = 0; i
Вот весь проект с симпатичным CLI: https://github.com/trichner/tcrypt
Редактировать: теперь с соответствующими encryptString
и decryptString