如何使用从密码派生的秘密密钥正确加密和解密文件

huangapple go评论146阅读模式
英文:

How to correctly encrypt and decrypt a file using a secret key derived from a password

问题

我正试图找出使用“PBEWithHmacSHA256AndAES_256”标准加密和解密文件的正确过程。

根据我从查看Oracle的示例代码所理解的,我已经得出需要盐(salt)、迭代次数和哈希标准的结论。

因此,我有了主要的方法,将以下内容传递到加密方法中:

  1. 作为字节数组传入的用户定义密码new String(key).toCharArray()(对于其他加密运行使用此方法)。
  2. 作为字节数组传入的安全随机IVinitVector
  3. 作为字符串传入的明文文件inputFile
  4. 作为字符串传入的待创建的密文文件名称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");
}

然后我也有我的主要方法,传递到解密方法中:

  1. 作为字符串传入的用户定义密码String inputKEY(对于其他解密运行也使用此方法)。
  2. 作为字符串传入的inputIV,已传入null,因为在PBE中不使用它。
  3. 作为字符串传入的密文文件inputFile
  4. 作为字符串传入的待创建的明文文件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:

  1. a user-defined password new String(key).toCharArray() as a byte array (using this method for other encryption runs)
  2. a secure random IV initVector as a byte array
  3. the plain text file inputFile as a String
  4. 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:

  1. a user-defined password String inputKEY as a string (also using this method for other dencryption runs)

  2. a String for inputIV , has been passed in as null, since not used for PBE.

  3. the ciphertext file inputFile as a String

  4. the name of, to be created, revealplaintext file outputFile as a String

    private 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 字节。

另外:

  • 在编码/解码过程中,有时没有指定编码方式,因此会使用默认编码。
  • encryptdecrypt 使用了不同类型的参数来表示密钥和 IV。
  • 代码中存在许多复制粘贴错误。

注意:与您的代码不同,链接中的代码除了从密码和盐值中确定密钥外,还确定了 IV。在您的代码中,IV 被传递。因此,您必须确保密钥/IV 对只能使用一次。通常会为每次加密生成一个随机的 IV。

以下代码(基于您的代码,但为了简单起见,省略了异常处理部分)修复了/优化了这些问题。此外,代码使用了 FileInputStreamFileOutputStream 而不是您的类(但这不是必需的):

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 in decrypt.
  • The 16 bytes salt is stored in encrypt (unnecessarily) Base64 encoded, i.e. 24 bytes are stored. In decrypt however only 16 bytes are loaded.

Also:

  • Upon encoding/decoding, sometimes no encoding is specified, so the default encoding is used.
  • encrypt and decrypt 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(&quot;PBEWithHmacSHA256AndAES_256&quot;);
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&#39;s comment
PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count, iv);
// Cipher
Cipher cipher = Cipher.getInstance(&quot;PBEWithHmacSHA256AndAES_256&quot;);
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(&quot;PBEWithHmacSHA256AndAES_256&quot;);
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(&quot;PBEWithHmacSHA256AndAES_256&quot;);
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​.

huangapple
  • 本文由 发表于 2020年9月3日 12:45:36
  • 转载请务必保留本文链接:https://go.coder-hub.com/63717001.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定