Вопрос-ответ

Java 256-bit AES Password-Based Encryption

Java 256-битное шифрование на основе пароля AES

Мне нужно реализовать 256-битное шифрование AES, но во всех примерах, которые я нашел в Интернете, используется "KeyGenerator" для генерации 256-битного ключа, но я хотел бы использовать свой собственный ключ доступа. Как я могу создать свой собственный ключ? Я попытался увеличить его до 256 бит, но затем я получаю сообщение об ошибке, в котором говорится, что ключ слишком длинный. У меня действительно установлен патч неограниченной юрисдикции, так что проблема не в этом :)

Ie. Генератор ключей выглядит следующим образом...

// Get the KeyGenerator
KeyGenerator kgen = KeyGenerator.getInstance("AES");
kgen.init(128); // 192 and 256 bits may not be available

// Generate the secret key specs.
SecretKey skey = kgen.generateKey();
byte[] raw = skey.getEncoded();

Код взят отсюда

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

На самом деле я увеличивал пароль до 256 байт, а не бит, что слишком длинно. Ниже приведен некоторый код, который я использую сейчас, когда у меня появилось больше опыта в этом.

byte[] key = null; // TODO
byte[] input = null; // TODO
byte[] output = null;
SecretKeySpec keySpec = null;
keySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
output = cipher.doFinal(input)

Биты "TODO", которые вам нужно выполнить самостоятельно :-)

Переведено автоматически
Ответ 1

Поделитесь password (a char[]) и salt (a byte[]-8 байт, выбранных a SecureRandom, — это хорошая соль, которую не нужно хранить в секрете) с получателем вне диапазона. Затем, чтобы получить хороший ключ из этой информации:

/* Derive the key, given password and salt. */
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
SecretKey tmp = factory.generateSecret(spec);
SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES");

Магические числа (которые где-то могут быть определены как константы) 65536 и 256 - это количество итераций получения ключа и размер ключа соответственно.

Функция получения ключа повторяется, что требует значительных вычислительных затрат, и это не позволяет злоумышленникам быстро пробовать множество разных паролей. Количество итераций может быть изменено в зависимости от доступных вычислительных ресурсов.

Размер ключа может быть уменьшен до 128 бит, что по-прежнему считается "надежным" шифрованием, но это не дает большого запаса прочности в случае обнаружения атак, ослабляющих AES.

При использовании правильного режима цепочки блоков один и тот же производный ключ может использоваться для шифрования множества сообщений. В цепочке блоков шифрования (CBC) для каждого сообщения генерируется случайный вектор инициализации (IV), который выдает различный зашифрованный текст, даже если обычный текст идентичен. CBC может быть не самым безопасным режимом, доступным для вас (см. AEAD ниже); существует множество других режимов с другими свойствами безопасности, но все они используют аналогичный случайный ввод. В любом случае результатами каждой операции шифрования являются зашифрованный текст и вектор инициализации:

/* Encrypt the message. */
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secret);
AlgorithmParameters params = cipher.getParameters();
byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV();
byte[] ciphertext = cipher.doFinal("Hello, World!".getBytes(StandardCharsets.UTF_8));

Сохраните ciphertext и iv. При расшифровке SecretKey восстанавливается точно таким же образом, используя использование пароля с теми же параметрами соли и итерации. Инициализируйте шифр этим ключом и вектором инициализации, сохраненным вместе с сообщением:

/* Decrypt the message, given derived key and initialization vector. */
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
String plaintext = new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
System.out.println(plaintext);

Java 7 включала API поддержку режимов шифрования AEAD, а поставщик "SunJCE", входящий в состав дистрибутивов OpenJDK и Oracle, реализует их, начиная с Java 8. Настоятельно рекомендуется использовать один из этих режимов вместо CBC; он защитит целостность данных, а также их конфиденциальность.


A java.security.InvalidKeyException с сообщением "Недопустимый размер ключа или параметры по умолчанию" означает, что уровень надежности криптографии ограничен; файлы политики юрисдикции с неограниченной надежностью находятся в неправильном расположении. В JDK они должны быть размещены в ${jdk}/jre/lib/security

Судя по описанию проблемы, файлы политики установлены некорректно. В системах может легко быть несколько сред выполнения Java; дважды проверьте, используется ли правильное расположение.

Ответ 2

Рассмотрите возможность использования криптомодуля Spring Security


Модуль Spring Security Crypto обеспечивает поддержку симметричного шифрования, генерации ключей и кодирования паролей. Код распространяется как часть основного модуля, но не зависит ни от какого другого кода Spring Security (или Spring).


Это обеспечивает простую абстракцию для шифрования и, похоже, соответствует тому, что требуется здесь,


"Стандартным" методом шифрования является 256-битный AES с использованием PBKDF2 от PKCS # 5 (функция получения ключа на основе пароля # 2). Для этого метода требуется Java 6. Пароль, используемый для генерации секретного ключа, должен храниться в надежном месте и не передаваться другим пользователям. Соль используется для предотвращения атак по словарю на ключ в случае, если ваши зашифрованные данные будут скомпрометированы. Также применяется 16-байтовый вектор случайной инициализации, поэтому каждое зашифрованное сообщение уникально.


Взгляд на внутренние компоненты обнаруживает структуру, аналогичную ответу Эриксона.

Как отмечено в вопросе, для этого также требуется политика юрисдикции неограниченной надежности Java Cryptography Extension (JCE) (иначе вы столкнетесь с InvalidKeyException: Illegal Key Size). Его можно загрузить для Java 6, Java 7 и Java 8.

Пример использования

import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.encrypt.TextEncryptor;
import org.springframework.security.crypto.keygen.KeyGenerators;

public class CryptoExample {
public static void main(String[] args) {
final String password = "I AM SHERLOCKED";
final String salt = KeyGenerators.string().generateKey();

TextEncryptor encryptor = Encryptors.text(password, salt);
System.out.println("Salt: \"" + salt + "\"");

String textToEncrypt = "*royal secrets*";
System.out.println("Original text: \"" + textToEncrypt + "\"");

String encryptedText = encryptor.encrypt(textToEncrypt);
System.out.println("Encrypted text: \"" + encryptedText + "\"");

// Could reuse encryptor but wanted to show reconstructing TextEncryptor
TextEncryptor decryptor = Encryptors.text(password, salt);
String decryptedText = decryptor.decrypt(encryptedText);
System.out.println("Decrypted text: \"" + decryptedText + "\"");

if(textToEncrypt.equals(decryptedText)) {
System.out.println("Success: decrypted text matches");
} else {
System.out.println("Failed: decrypted text does not match");
}
}
}

И пример вывода,

Соль: "feacbc02a3a697b0"
Оригинальный текст: "* королевские секреты *"
Зашифрованный текст: "7c73c5a83fa580b5d6f8208768adc931ef3123291ac8bc335a1277a39d256d9a"
Расшифрованный текст: "* королевские секреты *"
Успешно: расшифрованный текст совпадает
Ответ 3

После прочтения предложений Эриксона и того, что я смог почерпнуть из пары других публикаций и этого примера здесь, я попытался обновить код Doug рекомендуемыми изменениями. Не стесняйтесь редактировать, чтобы сделать его лучше.


  • Вектор инициализации больше не исправлен

  • ключ шифрования получен с использованием кода erickson

  • 8-байтовая соль генерируется в setupEncrypt() с использованием SecureRandom()

  • ключ дешифрования генерируется на основе соли шифрования и пароля

  • шифр дешифрования генерируется на основе ключа дешифрования и вектора инициализации

  • удалено шестнадцатеричное преобразование вместо шестнадцатеричных подпрограмм кодека org.apache.commons

Некоторые примечания: Здесь используется 128-битный ключ шифрования - java, очевидно, не будет выполнять 256-битное шифрование "из коробки". Реализация 256 требует установки некоторых дополнительных файлов в каталог установки java.

Кроме того, я не сторонник криптовалют. Будьте внимательны.

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;

public class Crypto
{
String mPassword = null;
public final static int SALT_LEN = 8;
byte [] mInitVec = null;
byte [] mSalt = null;
Cipher mEcipher = null;
Cipher mDecipher = null;
private final int KEYLEN_BITS = 128; // see notes below where this is used.
private final int ITERATIONS = 65536;
private final int MAX_FILE_BUF = 1024;

/**
* create an object with just the passphrase from the user. Don't do anything else yet
* @param password
*/

public Crypto (String password)
{
mPassword = password;
}

/**
* return the generated salt for this object
* @return
*/

public byte [] getSalt ()
{
return (mSalt);
}

/**
* return the initialization vector created from setupEncryption
* @return
*/

public byte [] getInitVec ()
{
return (mInitVec);
}

/**
* debug/print messages
* @param msg
*/

private void Db (String msg)
{
System.out.println ("** Crypt ** " + msg);
}

/**
* this must be called after creating the initial Crypto object. It creates a salt of SALT_LEN bytes
* and generates the salt bytes using secureRandom(). The encryption secret key is created
* along with the initialization vectory. The member variable mEcipher is created to be used
* by the class later on when either creating a CipherOutputStream, or encrypting a buffer
* to be written to disk.
*
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws NoSuchPaddingException
* @throws InvalidParameterSpecException
* @throws IllegalBlockSizeException
* @throws BadPaddingException
* @throws UnsupportedEncodingException
* @throws InvalidKeyException
*/

public void setupEncrypt () throws NoSuchAlgorithmException,
InvalidKeySpecException,
NoSuchPaddingException,
InvalidParameterSpecException,
IllegalBlockSizeException,
BadPaddingException,
UnsupportedEncodingException,
InvalidKeyException
{
SecretKeyFactory factory = null;
SecretKey tmp = null;

// crate secureRandom salt and store as member var for later use
mSalt = new byte [SALT_LEN];
SecureRandom rnd = new SecureRandom ();
rnd.nextBytes (mSalt);
Db ("generated salt :" + Hex.encodeHexString (mSalt));

factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");

/* Derive the key, given password and salt.
*
* in order to do 256 bit crypto, you have to muck with the files for Java's "unlimted security"
* The end user must also install them (not compiled in) so beware.
* see here: http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files.shtml
*/

KeySpec spec = new PBEKeySpec (mPassword.toCharArray (), mSalt, ITERATIONS, KEYLEN_BITS);
tmp = factory.generateSecret (spec);
SecretKey secret = new SecretKeySpec (tmp.getEncoded(), "AES");

/* Create the Encryption cipher object and store as a member variable
*/

mEcipher = Cipher.getInstance ("AES/CBC/PKCS5Padding");
mEcipher.init (Cipher.ENCRYPT_MODE, secret);
AlgorithmParameters params = mEcipher.getParameters ();

// get the initialization vectory and store as member var
mInitVec = params.getParameterSpec (IvParameterSpec.class).getIV();

Db ("mInitVec is :" + Hex.encodeHexString (mInitVec));
}



/**
* If a file is being decrypted, we need to know the pasword, the salt and the initialization vector (iv).
* We have the password from initializing the class. pass the iv and salt here which is
* obtained when encrypting the file initially.
*
* @param initvec
* @param salt
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws InvalidAlgorithmParameterException
* @throws DecoderException
*/

public void setupDecrypt (String initvec, String salt) throws NoSuchAlgorithmException,
InvalidKeySpecException,
NoSuchPaddingException,
InvalidKeyException,
InvalidAlgorithmParameterException,
DecoderException
{
SecretKeyFactory factory = null;
SecretKey tmp = null;
SecretKey secret = null;

// since we pass it as a string of input, convert to a actual byte buffer here
mSalt = Hex.decodeHex (salt.toCharArray ());
Db ("got salt " + Hex.encodeHexString (mSalt));

// get initialization vector from passed string
mInitVec = Hex.decodeHex (initvec.toCharArray ());
Db ("got initvector :" + Hex.encodeHexString (mInitVec));


/* Derive the key, given password and salt. */
// in order to do 256 bit crypto, you have to muck with the files for Java's "unlimted security"
// The end user must also install them (not compiled in) so beware.
// see here:
// http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files.shtml
factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(mPassword.toCharArray (), mSalt, ITERATIONS, KEYLEN_BITS);

tmp = factory.generateSecret(spec);
secret = new SecretKeySpec(tmp.getEncoded(), "AES");

/* Decrypt the message, given derived key and initialization vector. */
mDecipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
mDecipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(mInitVec));
}


/**
* This is where we write out the actual encrypted data to disk using the Cipher created in setupEncrypt().
* Pass two file objects representing the actual input (cleartext) and output file to be encrypted.
*
* there may be a way to write a cleartext header to the encrypted file containing the salt, but I ran
* into uncertain problems with that.
*
* @param input - the cleartext file to be encrypted
* @param output - the encrypted data file
* @throws IOException
* @throws IllegalBlockSizeException
* @throws BadPaddingException
*/

public void WriteEncryptedFile (File input, File output) throws
IOException,
IllegalBlockSizeException,
BadPaddingException
{
FileInputStream fin;
FileOutputStream fout;
long totalread = 0;
int nread = 0;
byte [] inbuf = new byte [MAX_FILE_BUF];

fout = new FileOutputStream (output);
fin = new FileInputStream (input);

while ((nread = fin.read (inbuf)) > 0 )
{
Db ("read " + nread + " bytes");
totalread += nread;

// create a buffer to write with the exact number of bytes read. Otherwise a short read fills inbuf with 0x0
// and results in full blocks of MAX_FILE_BUF being written.
byte [] trimbuf = new byte [nread];
for (int i = 0; i < nread; i++)
trimbuf[i] = inbuf[i];

// encrypt the buffer using the cipher obtained previosly
byte [] tmp = mEcipher.update (trimbuf);

// I don't think this should happen, but just in case..
if (tmp != null)
fout.write (tmp);
}

// finalize the encryption since we've done it in blocks of MAX_FILE_BUF
byte [] finalbuf = mEcipher.doFinal ();
if (finalbuf != null)
fout.write (finalbuf);

fout.flush();
fin.close();
fout.close();

Db ("wrote " + totalread + " encrypted bytes");
}


/**
* Read from the encrypted file (input) and turn the cipher back into cleartext. Write the cleartext buffer back out
* to disk as (output) File.
*
* I left CipherInputStream in here as a test to see if I could mix it with the update() and final() methods of encrypting
* and still have a correctly decrypted file in the end. Seems to work so left it in.
*
* @param input - File object representing encrypted data on disk
* @param output - File object of cleartext data to write out after decrypting
* @throws IllegalBlockSizeException
* @throws BadPaddingException
* @throws IOException
*/

public void ReadEncryptedFile (File input, File output) throws
IllegalBlockSizeException,
BadPaddingException,
IOException
{
FileInputStream fin;
FileOutputStream fout;
CipherInputStream cin;
long totalread = 0;
int nread = 0;
byte [] inbuf = new byte [MAX_FILE_BUF];

fout = new FileOutputStream (output);
fin = new FileInputStream (input);

// creating a decoding stream from the FileInputStream above using the cipher created from setupDecrypt()
cin = new CipherInputStream (fin, mDecipher);

while ((nread = cin.read (inbuf)) > 0 )
{
Db ("read " + nread + " bytes");
totalread += nread;

// create a buffer to write with the exact number of bytes read. Otherwise a short read fills inbuf with 0x0
byte [] trimbuf = new byte [nread];
for (int i = 0; i < nread; i++)
trimbuf[i] = inbuf[i];

// write out the size-adjusted buffer
fout.write (trimbuf);
}

fout.flush();
cin.close();
fin.close ();
fout.close();

Db ("wrote " + totalread + " encrypted bytes");
}


/**
* adding main() for usage demonstration. With member vars, some of the locals would not be needed
*/

public static void main(String [] args)
{

// create the input.txt file in the current directory before continuing
File input = new File ("input.txt");
File eoutput = new File ("encrypted.aes");
File doutput = new File ("decrypted.txt");
String iv = null;
String salt = null;
Crypto en = new Crypto ("mypassword");

/*
* setup encryption cipher using password. print out iv and salt
*/

try
{
en.setupEncrypt ();
iv = Hex.encodeHexString (en.getInitVec ()).toUpperCase ();
salt = Hex.encodeHexString (en.getSalt ()).toUpperCase ();
}
catch (InvalidKeyException e)
{
e.printStackTrace();
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
catch (InvalidKeySpecException e)
{
e.printStackTrace();
}
catch (NoSuchPaddingException e)
{
e.printStackTrace();
}
catch (InvalidParameterSpecException e)
{
e.printStackTrace();
}
catch (IllegalBlockSizeException e)
{
e.printStackTrace();
}
catch (BadPaddingException e)
{
e.printStackTrace();
}
catch (UnsupportedEncodingException e)
{
e.printStackTrace();
}

/*
* write out encrypted file
*/

try
{
en.WriteEncryptedFile (input, eoutput);
System.out.printf ("File encrypted to " + eoutput.getName () + "\niv:" + iv + "\nsalt:" + salt + "\n\n");
}
catch (IllegalBlockSizeException e)
{
e.printStackTrace();
}
catch (BadPaddingException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}


/*
* decrypt file
*/

Crypto dc = new Crypto ("mypassword");
try
{
dc.setupDecrypt (iv, salt);
}
catch (InvalidKeyException e)
{
e.printStackTrace();
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
catch (InvalidKeySpecException e)
{
e.printStackTrace();
}
catch (NoSuchPaddingException e)
{
e.printStackTrace();
}
catch (InvalidAlgorithmParameterException e)
{
e.printStackTrace();
}
catch (DecoderException e)
{
e.printStackTrace();
}

/*
* write out decrypted file
*/

try
{
dc.ReadEncryptedFile (eoutput, doutput);
System.out.println ("decryption finished to " + doutput.getName ());
}
catch (IllegalBlockSizeException e)
{
e.printStackTrace();
}
catch (BadPaddingException e)
{
e.printStackTrace();
}
catch (IOException e)
{
e.printStackTrace();
}
}


}
Ответ 4

Легко сгенерировать свой собственный ключ из массива байтов:

byte[] raw = ...; // 32 bytes in size for a 256 bit key
Key skey = new javax.crypto.spec.SecretKeySpec(raw, "AES");

Но создания 256-битного ключа недостаточно. Если генератор ключей не может сгенерировать для вас 256-битные ключи, то Cipher класс, вероятно, также не поддерживает 256-битный AES. Вы говорите, что у вас установлен патч неограниченной юрисдикции, поэтому шифр AES-256 должен поддерживаться (но тогда должны быть и 256-битные ключи, так что это может быть проблемой конфигурации).

Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, skey);
byte[] encrypted = cipher.doFinal(plainText.getBytes());

Обходной путь из-за отсутствия поддержки AES-256 заключается в том, чтобы взять какую-нибудь свободно доступную реализацию AES-256 и использовать ее в качестве пользовательского поставщика. Для этого необходимо создать свой собственный Provider подкласс и использовать его с Cipher.getInstance(String, Provider). Но это может быть сложным процессом.

java