英文:
How to correctly encrypt and decrypt a file using a secret key derived from a password
问题
我正试图找出使用“PBEWithHmacSHA256AndAES_256”标准加密和解密文件的正确过程。
根据我从查看Oracle的示例代码所理解的,我已经得出需要盐(salt)、迭代次数和哈希标准的结论。
因此,我有了主要的方法,将以下内容传递到加密方法中:
- 作为字节数组传入的用户定义密码
new String(key).toCharArray()
(对于其他加密运行使用此方法)。 - 作为字节数组传入的安全随机IV
initVector
。 - 作为字符串传入的明文文件
inputFile
。 - 作为字符串传入的待创建的密文文件名称
outputFile
。
我已经根据代码示例编写了我认为正确的加密方法。我将盐和IV存储起来,以便在密文中将它们附加在一起。
以下是我认为正确的加密方法的代码:
private static void encrypt(byte[] key, byte[] initVector, String inputFile, String outputFile) //exceptions for throws... {
// 初始化加密
Cipher cipher;
byte[] salt = new byte[16];
SecureRandom rand = new SecureRandom();
// 使用Base64随机生成盐
rand.nextBytes(salt);
System.out.println("my salt should be" + Base64.getEncoder().encodeToString(salt));
salt = Base64.getEncoder().encode(salt);
// 迭代次数
int count = 1000;
IvParameterSpec iv = new IvParameterSpec(initVector);
// 创建PBE参数集
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(Base64.getDecoder().decode(salt), count, iv);
// 将密码转换为SecretKey对象
PBEKeySpec pbeKeySpec = new PBEKeySpec(new String(key).toCharArray());
SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKey pbeKey;
try {
pbeKey = keyFac.generateSecret(pbeKeySpec);
} catch (InvalidKeySpecException e) {
System.out.println("Sorry, the password specified cannot be used as a secret key");
System.out.println("Please check that your password uses valid characters");
return;
}
// 创建PBE Cipher
cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
// 使用密钥和参数初始化PBE Cipher
cipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
// 文件错误检查和文件处理(生成文件路径)...
System.out.println("Secret key is " + Base64.getEncoder().encodeToString(key));
System.out.println("IV is " + Base64.getEncoder().encodeToString(initVector));
// 使用'Cipher Stream'进行特殊文件读取和写入
try (InputStream fin = FileEncryptor.class.getResourceAsStream(loadFile.getName());
OutputStream fout = Files.newOutputStream(saveFile);
CipherOutputStream cipherOut = new CipherOutputStream(fout, cipher)) {
final byte[] bytes = new byte[1024];
for (int length = fin.read(bytes); length != -1; length = fin.read(bytes)) {
fout.write(initVector);
fout.write(salt);
cipherOut.write(bytes, 0, length);
}
} catch (IOException e) {
System.out.println("Something went wrong with reading and writing these files!");
System.out.println("Please check you have the latest version of this program");
System.out.println("Contact your IT admin to make sure you have sufficient privileges");
}
System.out.println("SUCCESS! Encryption finished, saved at specified location");
}
然后我也有我的主要方法,传递到解密方法中:
- 作为字符串传入的用户定义密码
String inputKEY
(对于其他解密运行也使用此方法)。 - 作为字符串传入的
inputIV
,已传入null,因为在PBE中不使用它。 - 作为字符串传入的密文文件
inputFile
。 - 作为字符串传入的待创建的明文文件
outputFile
。
以下是我认为正确的解密方法的代码:
private static void decrypt(String inputKEY, String inputIV, String inputFile, String outputFile) {
Cipher cipher = null;
// 文件错误检查和文件处理(生成文件路径)...
InputStream encryptedData = Files.newInputStream(loadFilePath);
PBEKeySpec pbeKeySpec = new PBEKeySpec(inputKEY.toCharArray());
SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKey pbeKey = null;
try {
pbeKey = keyFac.generateSecret(pbeKeySpec);
} catch (InvalidKeySpecException e) {
e.printStackTrace();
}
byte[] initVect = new byte[16];
encryptedData.read(initVect);
IvParameterSpec iv = new IvParameterSpec(Base64.getDecoder().decode(initVect));
byte[] salt = new byte[16];
encryptedData.read(salt);
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(Base64.getDecoder().decode(salt), 1000, iv);
cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
System.out.println("my salt should be" + Base64.getEncoder().encodeToString(Base64.getDecoder().decode(salt)));
cipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec);
try (CipherInputStream decryptStream = new CipherInputStream(encryptedData, cipher);
OutputStream decryptedOut = Files.newOutputStream(saveFile)) {
final byte[] bytes = new byte[1024];
for (int length = decryptStream.read(bytes); length != -1; length = decryptStream.read(bytes)) {
decryptedOut.write(bytes, 0, length);
}
} catch (IOException e) {
System.out.println("Something went wrong with reading and writing these files!");
System.out.println("Please check you have the latest version of this program");
System.out.println("Contact your IT admin to make sure you have sufficient privileges");
}
System.out.println("SUCCESS! Decryption finished, saved at specified location");
}
我认为我对PBE的理解可能存在问题,因此我实现的方式可能是错误的。有谁可以指出似乎有什么问题吗?
英文:
I am trying to work out the correct process to encrypt and decrypt a file using the "PBEWithHmacSHA256AndAES_256" standard.
From what I understand from looking at this example code from Oracle.
I have gathered that a salt is needed, as well as an iteration count and the hash standard.
So I have my main method, passing into the encryption method:
- a user-defined password
new String(key).toCharArray()
as a byte array (using this method for other encryption runs) - a secure random IV
initVector
as a byte array - the plain text file
inputFile
as a String - the name of, to be created, ciphertext file
outputFile
as a String
I have followed the code example to code up what I believe is correct for the encrypt method. And I am storing the salt and IV to be used for decryption by appending them both to the ciphertext.
private static void encrypt(byte[] key, byte[] initVector, String inputFile, String outputFile) //exceptions for throws... {
//Initalisation for encryption
Cipher cipher;
byte[] salt = new byte[16];
SecureRandom rand = new SecureRandom();
// Salt randomly generated with base64
rand.nextBytes(salt);
System.out.println("my salt should be" + Base64.getEncoder().encodeToString(salt));
salt = Base64.getEncoder().encode(salt);
// Iteration count
int count = 1000;
IvParameterSpec iv = new IvParameterSpec(initVector);
// Create PBE parameter set
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(Base64.getDecoder().decode(salt), count, iv);
// Convert pass into SecretKey object
PBEKeySpec pbeKeySpec = new PBEKeySpec(new String(key).toCharArray());
SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKey pbeKey;
try {
pbeKey = keyFac.generateSecret(pbeKeySpec);
} catch (InvalidKeySpecException e) {
System.out.println("Sorry, the password specified cannot be used as a secret key");
System.out.println("Please check that your password uses valid characters");
return;
}
// Create PBE Cipher
cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
// Initialize PBE Cipher with key and parameters
cipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
}
//File error checking and file handling (i.e. generating file paths)...
System.out.println("Secret key is " + Base64.getEncoder().encodeToString(key));
System.out.println("IV is " + Base64.getEncoder().encodeToString(initVector));
//Special file reading and writing with 'Cipher Stream'
try (InputStream fin = FileEncryptor.class.getResourceAsStream(loadFile.getName());
OutputStream fout = Files.newOutputStream(saveFile);
CipherOutputStream cipherOut = new CipherOutputStream(fout, cipher) {
}) {
final byte[] bytes = new byte[1024];
for(int length=fin.read(bytes); length!=-1; length = fin.read(bytes)){
fout.write(initVector);
fout.write(salt);
cipherOut.write(bytes, 0, length);
}
} catch (IOException e) {
System.out.println("Something went wrong with reading and writing these files!");
System.out.println("Please check you have the latest version of this program");
System.out.println("Contact your IT admin to make sure you have sufficient privileges");
}
System.out.println("SUCCESS! Encryption finished, saved at specified location");
}
Then I also have my main method, passing into the decryption method:
-
a user-defined password
String inputKEY
as a string (also using this method for other dencryption runs) -
a String for
inputIV
, has been passed in as null, since not used for PBE. -
the ciphertext file
inputFile
as a String -
the name of, to be created, revealplaintext file
outputFile
as a Stringprivate static void decrypt(String inputKEY, String inputIV, String inputFile, String outputFile) {
Cipher cipher = null;//File error checking and file handling (i.e. generating file paths)... InputStream encryptedData = Files.newInputStream(loadFilePath); PBEKeySpec pbeKeySpec = new PBEKeySpec(inputKEY.toCharArray()); SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256"); SecretKey pbeKey = null; try { pbeKey = keyFac.generateSecret(pbeKeySpec); } catch (InvalidKeySpecException e) { // TODO Auto-generated catch block e.printStackTrace(); } byte[] initVect = new byte[16]; encryptedData.read(initVect); IvParameterSpec iv = new IvParameterSpec(Base64.getDecoder().decode(initVect); byte[] salt = new byte[16]; encryptedData.read(salt); PBEParameterSpec pbeParamSpec = new PBEParameterSpec(Base64.getDecoder().decode(salt), 1000, iv); cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256"); System.out.println("my salt should be" + Base64.getEncoder().encodeToString(Base64.getDecoder().decode(salt))); cipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec); try (CipherInputStream decryptStream = new CipherInputStream(encryptedData, cipher); OutputStream decryptedOut = Files.newOutputStream(saveFile)){ final byte[] bytes = new byte[1024]; for(int length=decryptStream.read(bytes); length!=-1; length = decryptStream.read(bytes)){ decryptedOut.write(bytes, 0, length); } } catch (IOException e) { //This is caught when decryption is run System.out.println("Something went wrong with reading and writing these files!"); System.out.println("Please check you have the latest version of this program"); System.out.println("Contact your IT admin to make sure you have sufficient privileges"); } System.out.println("SUCESS! Decryption finished, saved at specified location");
}
I believe something is not right with my understanding of PBE, and thus the way I implemented it is probably wrong. Could anyone point out what seems to be wrong?
答案1
得分: 9
以下是翻译好的内容:
主要问题包括:
- IV 和 Salt 不能写在
for
循环内部。 - IV 在
encrypt
中存储为未经 Base64 编码的形式,但在decrypt
中进行了 Base64 解码。 - 16 字节的盐在
encrypt
中以 Base64 编码的形式存储(不必要),即存储了 24 字节。然而,在decrypt
中只加载了 16 字节。
另外:
- 在编码/解码过程中,有时没有指定编码方式,因此会使用默认编码。
encrypt
和decrypt
使用了不同类型的参数来表示密钥和 IV。- 代码中存在许多复制粘贴错误。
注意:与您的代码不同,链接中的代码除了从密码和盐值中确定密钥外,还确定了 IV。在您的代码中,IV 被传递。因此,您必须确保密钥/IV 对只能使用一次。通常会为每次加密生成一个随机的 IV。
以下代码(基于您的代码,但为了简单起见,省略了异常处理部分)修复了/优化了这些问题。此外,代码使用了 FileInputStream
和 FileOutputStream
而不是您的类(但这不是必需的):
private static void encrypt(String key, byte[] initVector, String inputFile, String outputFile) throws Exception {
// 密钥
PBEKeySpec pbeKeySpec = new PBEKeySpec(key.toCharArray());
SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);
// IV
IvParameterSpec iv = new IvParameterSpec(initVector);
// 盐
SecureRandom rand = new SecureRandom();
byte[] salt = new byte[16];
rand.nextBytes(salt);
// 参数规范
int count = 1000; // 应该更大,参见 Michael Fehr 的评论
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count, iv);
// 密码器
Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
cipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
try (FileInputStream fin = new FileInputStream(inputFile);
FileOutputStream fout = new FileOutputStream(outputFile);
CipherOutputStream cipherOut = new CipherOutputStream(fout, cipher)) {
// 写入 IV、盐
fout.write(initVector);
fout.write(salt);
// 加密
final byte[] bytes = new byte[1024];
for (int length = fin.read(bytes); length != -1; length = fin.read(bytes)) {
cipherOut.write(bytes, 0, length);
}
}
}
private static void decrypt(String key, byte[] initVect, String inputFile, String outputFile) throws Exception {
try (FileInputStream encryptedData = new FileInputStream(inputFile);
FileOutputStream decryptedOut = new FileOutputStream(outputFile)) {
// 密钥
PBEKeySpec pbeKeySpec = new PBEKeySpec(key.toCharArray());
SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);
// 读取 IV
if (initVect == null) {
initVect = encryptedData.readNBytes(16);
}
IvParameterSpec iv = new IvParameterSpec(initVect);
// 读取盐
byte[] salt = encryptedData.readNBytes(16);
// 参数规范
int count = 1000;
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count, iv);
// 密码器
Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
cipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec);
try (CipherInputStream decryptStream = new CipherInputStream(encryptedData, cipher)) {
// 解密
final byte[] bytes = new byte[1024];
for (int length = decryptStream.read(bytes); length != -1; length = decryptStream.read(bytes)) {
decryptedOut.write(bytes, 0, length);
}
}
}
}
编辑 - 关于在 decrypt
中读取盐和 IV 的问题:
正如 GPI 在他们的评论中指出的那样,FileInputStream.read(byte[] b)
通常会读取 b.length
字节,但这并不是保证的。更健壮的方法是确定读取数据的长度,并在循环中调用该方法,直到数据完整。另一种替代方法是使用 InputStream.readNBytes(int len)
,这个方法保证会读取 len
字节(除非遇到流的末尾或抛出异常),正如 Zabuzard 所建议的。在代码中,现在使用了后者,即将 read
替换为 readNBytes
。
英文:
The main issues are:
- IV and Salt must not be written inside the
for
loop. - The IV is stored in
encrypt
not Base64 encoded, but is Base64 decoded indecrypt
. - The 16 bytes salt is stored in
encrypt
(unnecessarily) Base64 encoded, i.e. 24 bytes are stored. Indecrypt
however only 16 bytes are loaded.
Also:
- Upon encoding/decoding, sometimes no encoding is specified, so the default encoding is used.
encrypt
anddecrypt
use different parameter types for key and IV.- There are many copy/paste errors in the code.
Note: In contrast to your code, the linked code determines besides the key also the IV from password and salt.
In your code the IV is passed. Thus you have to ensure that a key/IV pair may only be used once. Usually a random IV is generated for each encryption.
In the following code (which is based on your code, but for simplicity without exception handling) these issues are fixed/optimized. Furthermore, the code applies FileInputStream
and FileOutputStream
instead of your classes (but this is not required):
<!-- language: none -->
private static void encrypt(String key, byte[] initVector, String inputFile, String outputFile) throws Exception {
// Key
PBEKeySpec pbeKeySpec = new PBEKeySpec(key.toCharArray());
SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);
// IV
IvParameterSpec iv = new IvParameterSpec(initVector);
// Salt
SecureRandom rand = new SecureRandom();
byte[] salt = new byte[16];
rand.nextBytes(salt);
// ParameterSpec
int count = 1000; // should be larger, see Michael Fehr's comment
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count, iv);
// Cipher
Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
cipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec);
try (FileInputStream fin = new FileInputStream(inputFile);
FileOutputStream fout = new FileOutputStream(outputFile);
CipherOutputStream cipherOut = new CipherOutputStream(fout, cipher)) {
// Write IV, Salt
fout.write(initVector);
fout.write(salt);
// Encrypt
final byte[] bytes = new byte[1024];
for (int length = fin.read(bytes); length != -1; length = fin.read(bytes)) {
cipherOut.write(bytes, 0, length);
}
}
}
<!-- language: none -->
<!-- language: none -->
private static void decrypt(String key, byte[] initVect, String inputFile, String outputFile) throws Exception {
try (FileInputStream encryptedData = new FileInputStream(inputFile);
FileOutputStream decryptedOut = new FileOutputStream(outputFile)) {
// Key
PBEKeySpec pbeKeySpec = new PBEKeySpec(key.toCharArray());
SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithHmacSHA256AndAES_256");
SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec);
// Read IV
if (initVect == null) {
initVect = encryptedData.readNBytes(16);
}
IvParameterSpec iv = new IvParameterSpec(initVect);
// Read salt
byte[] salt = encryptedData.readNBytes(16);
// ParameterSpec
int count = 1000;
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count, iv);
// Cipher
Cipher cipher = Cipher.getInstance("PBEWithHmacSHA256AndAES_256");
cipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec);
try (CipherInputStream decryptStream = new CipherInputStream(encryptedData, cipher)) {
// Decrypt
final byte[] bytes = new byte[1024];
for (int length = decryptStream.read(bytes); length != -1; length = decryptStream.read(bytes)) {
decryptedOut.write(bytes, 0, length);
}
}
}
}
<!-- language: none -->
EDIT - Concerning the reading of salt and IV in decrypt
:<br>As GPI pointed out in their comment, FileInputStream.read(byte[] b)
generally reads b.length
bytes, but this is not guaranteed. More robust is to determine the length of the read data and call the method in a loop until the data is complete. Another alternative is the use of InputStream.readNBytes(int len)
, which is guaranteed to read len
bytes (unless end of stream is encountered or an exception is thrown), as Zabuzard has suggested. In the code, the latter is now used, i.e. read
was replaced by readNBytes
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论