【问题标题】:Best way to decrypt a file on the fly动态解密文件的最佳方法
【发布时间】:2020-09-02 00:09:53
【问题描述】:

我有一个逐行加密的加密 CSV 文件。即:

  • encrypted_row1
  • encrypted_row2
  • encrypted_row3

加密密钥:3B047F64FC0DC09ECFA7C59C2C84FBB703F074474459B4A8690A9229297B77F6 示例文件:

FE8C7D902C44957624DBA215F4DE1DF08E5B44A29E57176C222D1B040201ED520C920063A8F83A623EC7590F96DEB9E714DF52E826E1219915936765D86C19FEC8B7974023711A9706458A203BADEA36A44530A262B470D0983716A5A533472A 03269E5BC0BC1BF9411E44A0AE7E9490AE27F1E42770020B342BCB1171F1D757AFDAC75F7FA8F6C753CA9DA5AA831FD4878C761ECEBBE261BE0D67B12F15DBFB03311C72613816AF19174 F8E7A3201B9DDFAA11A3017DC3756706E832DA95A387C7889FE37DB586A20102793E70A7378A54AFFEF1E1239F56BD1589A7B9348847D1BE7A78759C93BA6A534F83A0C1C10FC53 CF813CADE9778381CF68E613E6E9D86E22F0C413A76A4B6C429FB9EDA20C7F2582B293D35FD2206C6E7AEB96464DA0EF22005D019274E3AC32A5861D7C068EFFA5395D86DEB48C4A31E928A7B9720708

我有一个以 java.io.Reader 作为输入的函数。我需要解密此文件,然后即时提供解密文件内容的 java.io.Reader。

我的代码:

    String path = "path-to-file";
    File file =  new File(path);
    
    String encryptionKey = "encryptionKey";
    SecretKeySpec secKeySpec = new SecretKeySpec(Hex.decode(encryptionKey), "AES"); 
    byte[] ivBytes = new byte[16];
    IvParameterSpec ivParameterSpec = new IvParameterSpec(ivBytes);
    Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");                                   
  
    cipher.init(Cipher.DECRYPT_MODE, secKeySpec, ivParameterSpec);
    
    Reader reader = new InputStreamReader(new FileInputStream(file), UTF_8);
    
    BufferedReader rd = new BufferedReader(reader);
    
    String line = "";
    while ((line = rd.readLine()) != null) {
        System.out.println(new String(cipher.doFinal(Hex.decode(line))));
    }

我需要以最有效的方式将 HEX 解码和解密的字符串包装在 java.io.Reader 中。 (文件大小可能非常大)。 我有一个使用 java.io.Reader 读取其字符以对其执行一些业务逻辑的函数。 我不想解密并写入文件然后重新读取它。我需要即时或以安全的方式执行此操作

最好的方法是什么?

【问题讨论】:

  • 您在cihper.doFinal(...) 中解密。为什么不在那里实现您的业务逻辑?
  • 您能否更具体地了解要求?无需提供 java.io.Reader 即可实现逐行处理(即解密和应用业务逻辑)文件的能力,而只需提供一个简单的 Iterator(可能基于 Reader,为什么不提供)。那么你需要你的解密 API 是一个 java.io.Reader,还是你只需要它是一个迭代器(next() 语义)? Reader 语义更难实现(例如,仅读取一个字符是 Reader API 的一部分,但如果您只需要行,那么为什么要尝试符合 Reader API 呢?)
  • 另一个问题:字符串格式的十六进制数据是否受 限制?如果是,它只是一个简单的文本行阅读器,然后是转换“hexStringToByteArray”(您会在 SO 上找到十几个示例)并将 byte[] 提供给您的解密方法。
  • 每一行是单个加密块还是加密块可以与下一行重叠?
  • @GPI 我有 CSV 阅读器,它以 java.io.Reader 作为输入,我无法更改此 CSV 阅读器,因为它被其他类使用。在加密之前,我们从 FileInputStream 传递了一个 InputStreamReader 给 CSV 读取方法 - 加密之后,每一行都是加密后的行数据的 HEX 字符串。我需要解码十六进制并解密它。解码/解密是逐行完成的,而我的 CSV 阅读器使用输入 java.io.Reader 并逐个字符地读取它。所以我需要将 InputStreamReader 包装到另一个阅读器中,该阅读器逐行解密,readChar() 返回解密后的字符。

标签: java file encryption optimization


【解决方案1】:

这可以通过 CipherInputStream/CipherOutputStream 类来实现。

但是,这里有一个重要的警告:某些加密模式具有身份验证功能,如果身份验证是您的加密模式的一部分, 那么就不可能在运行中安全地解密。

要点如下:如果攻击者向您发送消息,并且您开始根据您解密的数据运行代码,但您不知道数据是真实的,那么数据很可能是恶意的。因此,要对加密数据采取安全措施,您必须在对收到的数据采取措施之前解密并验证身份验证。

【讨论】:

  • 同意消息认证。但这可能不是问题。请记住,它不是加密的文件,而是每一行。我的收获是,如果您一次解密整行,您可以并且将会检测到身份验证错误。所以从这个意义上说,你应该能够逐行处理文件。
  • @sonOfRa:在用户给出的示例中,算法模式是“AES/CBC/NoPadding”,所以似乎没有使用身份验证。
  • 这是正确的,尽管这将是下一个要解决的问题;几乎所有情况下,未经身份验证的加密都是不安全的。我们需要来自@selim-alawa 的更多信息
  • @sonOfRa 我尝试过使用 CipherInputStream,但它不起作用。它要么在不解码的情况下解密,因此得到错误的值,或者由于输入长度不是 16 字节的倍数而引发 IllegalBlockSizeException。我将 FileInputStream 包装在密码输入流中 new InputStreamReader(new CipherInputStream(new FileInputStream(file), cipher), UTF_8);
  • @sonOfRa 没有认证
【解决方案2】:

您的用例是标准流/阅读器实现中不完全可用的工具的混合体。

特别之处在于您从读取器进程(加密文件的行)开始,然后每一行都必须回退到基于字节的进程(密码操作),并且您希望输出具有字符语义( reader) 传递给 CSV 解析器。这个 char/byte/char 部分不是微不足道的。

我将使用的过程是创建另一个阅读器,我称之为LineByLineProcessingReader,它允许逐行使用输入,然后对其进行处理,然后将每行的输出作为阅读器提供。

这个过程对于这个目的真的无关紧要。您的流程实现将是十六进制解码,然后解密,然后转换回字符串,但它很可能只是任何东西。

棘手的部分在于使流程符合 Reader API。

当符合 Reader API 时,您可以选择扩展 FilterReaderReader 本身。通常是过滤器版本。

我选择不扩展过滤器版本,因为总的来说,我的过程是永远不会暴露原始文件的内容。由于过滤器的实现总是回退到原始读者的实现,因此这种制作将意味着重新实现所有内容,这会产生大量工作。

另一方面,如果我直接覆盖Reader,我只有一个方法可以正确,因为Reader真正要表达的是read(buf, o, c)方法,所有其他的都是在它之上实现的。

我选择实现它的策略类似于在某些数据源上创建自己的Iterator 实现。我预取输入文件的一行,并将其作为Reader 提供在一个内部变量中。

完成后,我只需要确保每次完全读取前一个读取器变量(例如当前行)时始终预取此读取器变量,而不是之前。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

public class LineByLineProcessingReader extends Reader {

    /** What to use as a line break. Becuase bufferedReader does not report the line breaks, we insert them manually using this */
    final String lineBreak;
    /** The original input being read */
    final BufferedReader input;

    /** A reader for the current processed line. */
    private StringReader currentReader;
    private boolean closedOrFinished = false;

    /**
     * Creates a reader that will ingest an input line by line,
     * then process it (default implementation is a no-op),
     * then recreate a reader for it, until there is no more line.
     *
     * @param in a Reader object providing the underlying stream.
     * @throws NullPointerException if <code>in</code> is <code>null</code>
     */
    protected LineByLineProcessingReader(BufferedReader in, String lineBreak) {
        this.input = in;
        this.lineBreak = lineBreak;
    }

    public int read(char[] cbuf, int off, int len) throws IOException {
        ensureNextLine();
        // Check end of input
        if (currentReader == null) {
            return -1;
        }
        int read = currentReader.read(cbuf, off, len);
        // Edge case : if current reader was at its end
        if (read < 0) {
            currentReader = null;
            // Recurse to go fetch next line.
            return read(cbuf, off, len);
        }
        // General case, we have our result.
        // We may have read less than was asked (in length), but it's contractually OK.
        return read;
    }

    /**
     * Advances the underlying input to the next line, and makes it available
     * for reading inside the {@link #currentReader}
     */
    private void ensureNextLine() throws IOException {
        // Do not try to read if closed or already finished
        if (closedOrFinished) {
            return;
        }
        // Check if there is still data to be read
        if (currentReader != null) {
            return;
        }

        String nextLine = input.readLine();
        if (nextLine == null) {
            // Nothing was left to read, we are bailing out.
            currentReader = null;
            closedOrFinished = true;
            return;
        }
        // We have a new line, process it and publish it as a reader
        String processedLine = processRawLine(nextLine);
        currentReader = new StringReader(processedLine+lineBreak);
    }

    /**
     * Performs a process of the raw line read from the underlying source.
     * @param rawLine the raw line read
     * @return a processed line
     */
    protected String processRawLine(String rawLine) {
        return rawLine;
    }


    @Override
    public void close() throws IOException {
        input.close();
        closedOrFinished = true;
    }


}

剩下要做的就是将您的解密过程插入processLine 方法中。

对课程的快速测试(您可能需要进一步检查)。

import junit.framework.TestCase;
import org.junit.Assert;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class LineByLineProcessingReaderTest extends TestCase {

    public void testRead() throws IOException {
        String input = "a\nb";

        // Reading a char one by one
        try (Reader r = new LineByLineProcessingReader(new BufferedReader(new StringReader(input)), "\n")) {
            String oneByOne = readExcatlyCharByChar(r, 3);
            Assert.assertEquals(input, oneByOne);
        }

        // Reading lines
        List<String> lines = readAllLines(
            new LineByLineProcessingReader(
                new BufferedReader(new StringReader(input)),
                "\n"
            )
        );
        Assert.assertEquals(Arrays.asList("a", "b"), lines);

        String[] moreComplexInput = new String[] {"Two households, both alike in dignity",
            "In fair Verona, where we lay our scene",
            "From ancient grudge break to new mutiny",
            "Where civil blood makes civil hands unclean." +
            "From forth the fatal loins of these two foes" +
            "A pair of star-cross'd lovers take their life;" +
            "Whose misadventured piteous overthrows" +
            "Do with their death bury their parents' strife." +
            "The fearful passage of their death-mark'd love",
            "And the continuance of their parents' rage",
            "Which, but their children's end, nought could remove",
            "Is now the two hours' traffic of our stage;" +
            "The which if you with patient ears attend",
            "What here shall miss, our toil shall strive to mend."};
        lines = readAllLines(new LineByLineProcessingReader(
            new BufferedReader(new StringReader(String.join("\n", moreComplexInput))), "\n") {
            @Override
            protected String processRawLine(String rawLine) {
                return rawLine.toUpperCase();
            }
        });
        Assert.assertEquals(Arrays.stream(moreComplexInput).map(String::toUpperCase).collect(Collectors.toList()), lines);
    }

    private String readExcatlyCharByChar(Reader reader,int numberOfReads) throws IOException {
        int nbRead = 0;
        try (StringWriter output = new StringWriter()) {
            while (nbRead < numberOfReads) {
                int read = reader.read();
                if (read < 0) {
                    throw new IOException("Expected " + numberOfReads + " but were only " + nbRead + " available");
                }
                output.write(read);
                nbRead++;
            }
            return output.toString();
        }
    }

    private List<String> readAllLines(Reader reader) throws IOException {
        try (BufferedReader b = new BufferedReader(reader)) {
            return b.lines().collect(Collectors.toList());
        }
    }

}

【讨论】:

  • 这看起来是一个很好的解决方案,我会测试它并告诉你。与此同时,我还想出了另一个解决方案,它实现了一个扩展缓冲读取器的 CipherBufferedReader。覆盖 read(char cbuf[], int off, int len) 方法。我使用 bufferedReader readLine 方法,读取一行,解密然后保存到缓冲区字符数组。您如何看待这个解决方案?
  • 尝试在阅读过程中随机调用skip(10),看看它是否仍然有效 :-) 这确实是唯一的问题:无论 read/readLine 的顺序/长度如何,您能否保留 Reader 的语义/skip/mark/reset 调用。
  • 调用skip(10)会导致IllegalBlockSizeException,而调用不同的值,即skip skip(129)会导致错误的解密值
  • 那是因为你的实现不够严格。例如。 skip(10) 可能会从原始(加密文件)中跳过 10 个字符,而不将它们呈现给密码,这会导致状态不一致并导致失败。如果你想走这条路,你必须确保 BufferedReader 的 所有 方法的行为一致(从未加密的数据中读取),并且调用加密文件的唯一方法是将数据提供给密码.这就是为什么我的实现隐藏原始阅读器,并且不过滤/扩展它。实现这种方式要简单得多。封装。
  • 使用 '\n' 应该没问题。我建议你一步一步调试。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-01-30
  • 2010-09-24
  • 2013-04-17
  • 2019-06-22
  • 1970-01-01
  • 1970-01-01
  • 2010-09-06
相关资源
最近更新 更多