如何使用从密码派生的密钥正确加密和解密文件
How to correctly encrypt and decrypt a file using a secret key derived from a password
我正在尝试找出使用“PBEWithHmacSHA256AndAES_256”标准加密和解密文件的正确过程。
据我了解来自 Oracle 的 example code。
我了解到需要加盐,以及迭代计数和哈希标准。
所以我有我的主要方法,传入加密方法:
- 用户定义的密码
new String(key).toCharArray()
作为字节数组(使用此方法进行其他加密运行)
- 作为字节数组的安全随机 IV
initVector
- 作为字符串的纯文本文件
inputFile
- 要创建的密文文件的名称
outputFile
作为字符串
我按照代码示例编写了我认为对加密方法正确的代码。我通过将盐和 IV 附加到密文来存储用于解密的盐和 IV。
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");
}
然后还有我的main方法,传入解密方法:
一个用户定义的密码String inputKEY
作为一个字符串(其他解密运行也使用此方法)
inputIV
的字符串,已作为 null 传入,因为未用于 PBE。
密文文件inputFile
作为字符串
要创建的以字符串形式显示明文文件的名称outputFile
private static void decrypt(String inputKEY, String inputIV, String inputFile, String outputFile) {
密码 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");
}
我认为我对PBE的理解有些不对,因此我实现它的方式可能是错误的。谁能指出似乎有什么问题?
主要问题是:
- IV 和 Salt 不能写在
for
循环中。
- IV 存储在
encrypt
中,未进行 Base64 编码,但在 decrypt
中进行了 Base64 解码。
- 16 字节的 salt 以
encrypt
(不必要地)Base64 编码存储,即存储 24 字节。然而在 decrypt
中只加载了 16 个字节。
还有:
- 在encoding/decoding之后,有时没有指定编码,所以使用默认编码。
encrypt
和 decrypt
对密钥和 IV 使用不同的参数类型。
- 代码中有很多 copy/paste 个错误。
注意:与您的代码不同,linked 代码除了确定密钥外,还确定来自密码和盐的 IV。
在您的代码中,IV 已通过。因此,您必须确保 key/IV 对只能使用一次。通常每次加密都会生成一个随机IV。
在下面的代码中(基于您的代码,但为简单起见,没有进行异常处理)这些问题是 fixed/optimized。此外,代码应用 FileInputStream
和 FileOutputStream
而不是您的 类 (但这不是必需的):
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);
}
}
}
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);
}
}
}
}
编辑 - 关于decrypt
中盐和IV的读数:
正如GPI指出的在他们的评论中,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)
,保证读取len
字节(除非遇到流结束或抛出异常),因为Zabuzard 已建议。在代码中,现在使用后者,即 read
被替换为 readNBytes
.
我正在尝试找出使用“PBEWithHmacSHA256AndAES_256”标准加密和解密文件的正确过程。
据我了解来自 Oracle 的 example code。
我了解到需要加盐,以及迭代计数和哈希标准。
所以我有我的主要方法,传入加密方法:
- 用户定义的密码
new String(key).toCharArray()
作为字节数组(使用此方法进行其他加密运行) - 作为字节数组的安全随机 IV
initVector
- 作为字符串的纯文本文件
inputFile
- 要创建的密文文件的名称
outputFile
作为字符串
我按照代码示例编写了我认为对加密方法正确的代码。我通过将盐和 IV 附加到密文来存储用于解密的盐和 IV。
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");
}
然后还有我的main方法,传入解密方法:
一个用户定义的密码
String inputKEY
作为一个字符串(其他解密运行也使用此方法)inputIV
的字符串,已作为 null 传入,因为未用于 PBE。密文文件
inputFile
作为字符串要创建的以字符串形式显示明文文件的名称
outputFile
private static void decrypt(String inputKEY, String inputIV, String inputFile, String outputFile) { 密码 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");
}
我认为我对PBE的理解有些不对,因此我实现它的方式可能是错误的。谁能指出似乎有什么问题?
主要问题是:
- IV 和 Salt 不能写在
for
循环中。 - IV 存储在
encrypt
中,未进行 Base64 编码,但在decrypt
中进行了 Base64 解码。 - 16 字节的 salt 以
encrypt
(不必要地)Base64 编码存储,即存储 24 字节。然而在decrypt
中只加载了 16 个字节。
还有:
- 在encoding/decoding之后,有时没有指定编码,所以使用默认编码。
encrypt
和decrypt
对密钥和 IV 使用不同的参数类型。- 代码中有很多 copy/paste 个错误。
注意:与您的代码不同,linked 代码除了确定密钥外,还确定来自密码和盐的 IV。 在您的代码中,IV 已通过。因此,您必须确保 key/IV 对只能使用一次。通常每次加密都会生成一个随机IV。
在下面的代码中(基于您的代码,但为简单起见,没有进行异常处理)这些问题是 fixed/optimized。此外,代码应用 FileInputStream
和 FileOutputStream
而不是您的 类 (但这不是必需的):
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);
}
}
}
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);
}
}
}
}
编辑 - 关于decrypt
中盐和IV的读数:
正如GPI指出的在他们的评论中,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)
,保证读取len
字节(除非遇到流结束或抛出异常),因为Zabuzard 已建议。在代码中,现在使用后者,即 read
被替换为 readNBytes
.