poi-original
A biblioteca Apache POI possibilita a leitura e gravação de documentos do Microsoft Office via código Java.

Por esses dias, fiz uma pesquisa sobre como essa biblioteca viabiliza (ou não) trabalharmos com arquivos protegidos do Excel.

Níveis de segurança do Office

Documentos do Excel podem ter diferentes níveis de proteção, a saber:

  • Proteção contra alteração: permite abrir o documento em modo somente-leitura, mas não deixa o usuário alterá-lo sem antes digitar a senha.
  • Proteção contra alteração de um trecho do documento: protege planilhas e células específicas com senha.
  • Proteção contra leitura: não permite abrir o documento sem a senha.

Todos esses modos podem ser usados ao mesmo tempo, inclusive com senhas diferentes.

Se o arquivo tiver proteção contra leitura, o Excel irá solicitar a senha antes de abrir o mesmo. Se houver proteção contra alteração ele irá solicitar a senha para desbloqueio da edição ou permitir acesso somente-leitura ao documento. Se planilhas ou células estiverem protegidas contra alteração, então o usuário deve acessar a função “Desproteger” no Excel e digitar a senha de desbloqueio para cada trecho.

As proteções contra alteração são simples travas no editor, pois o arquivo em si permanece aberto e não há restrições de alteração via programação ou com o uso de editores de terceiros. Podemos dizer que este mecanismo oferece um baixíssimo grau de segurança, que impedirá apenas o usuário mais leigo de efetivamente modificar o documento.

Por outro lado, a proteção contra leitura oferece um grau de segurança maior, pois o documento como um todo é criptografado. Isso significa que nenhum programa ou editor conseguirá sequer ler o documento sem antes descriptografar os bytes do arquivo com a senha original.

Versões dos documentos do Office

Quem acompanha o Office há algum tempo deve ter notado que, a partir da versão 2007, os documentos ganharam novas extensões, com o sufixo x. Por exemplo, doc passou a ser docx, xls mudou para xlsx e assim por diante.

Mas não foi apenas a extensão que mudou. O conteúdo foi completamente reformulado. O formato mais antigo era do tipo binário, enquanto os novos são baseados em XML.

As vantagens do formato XML são inúmeras, a começar pela capacidade de qualquer ferramenta que trabalha com XML conseguir processar o documento ou pelo menos parte dele.

No que se refere à biblioteca Apache POI, isso também nos afeta diretamente, pois há APIs diferentes para trabalhar com os diferentes formatos de documentos.

Como veremos adiante, nem todos os formatos suportam todos os tipos de segurança.

Capacidades do POI

A conclusão dos meus testes foi a seguinte:

Tipo de documento Operação Proteção contra alteração do documento Proteção contra alteração de uma planilha Proteção contra Leitura (criptografia)
XLS
Leitura OK
Criação N/D* OK N/D
XLSX
Leitura OK
Criação N/D OK OK

Na leitura de um arquivo XLS ou XLSX, as proteções contra alteração são ignoradas, então não há o que testar.
N/D = Não disponível, isto é, não há suporte na API.
* Existe um método para proteger o documento, mas ele não surte efeito (bug).

Como pode ser visto, para documentos no formato legado XLS o POI suporta apenas a leitura de arquivos criptografados. Na criação, ele apenas consegue proteger parte do documento contra alteração.

Já para o formato mais novo XLSX, o POI consegue ler e criar arquivos criptografados. Entretanto, na criação do documento, não há um método para proteger o documento todo contra alteração.

Alguns fóruns sugerem uma possibilidade de contornar a falta do recurso de proteger o documento como um todo contra alteração. Para isso, basta criar um arquivo Excel com tal proteção e usá-lo como template para geração de um novo.

Implementação

Criptografia em arquivos XLS

O POI suporta apenas ler arquivos criptografados neste formato, isto é, que estão protegidos contra leitura.

O segredo é a classe Biff8EncryptionKey. Basta definir a senha através do método estático setCurrentUserPassword e depois ler o arquivo normalmente.

Exemplo:

try {
    Biff8EncryptionKey.setCurrentUserPassword(password);
    new HSSFWorkbook(input);
} finally {
    Biff8EncryptionKey.setCurrentUserPassword(null);
}

É bom não esquecer de remover a senha ao final para não atrapalhar futuras leituras.

Mesmo sendo um método estático, não deve haver problemas de concorrências, pois a documentação afirma que a senha é armazenada usando ThreadLocal.

Criptografia em arquivos XLSX

O POI implementa tanto a leitura quanto a criação de documentos criptografados neste formato.

Veja minha implementação para a leitura:

public class XlsxDecryptor {

    public static XSSFWorkbook decrypt(InputStream input, String password)
            throws IOException {

        POIFSFileSystem filesystem = new POIFSFileSystem(input);
        EncryptionInfo info = new EncryptionInfo(filesystem);
        Decryptor d = Decryptor.getInstance(info);

        try {
            if (!d.verifyPassword(password)) {
                throw new RuntimeException("Unable to process: document is encrypted");
            }

            InputStream dataStream = d.getDataStream(filesystem);
            return new XSSFWorkbook(dataStream);
        } catch (GeneralSecurityException ex) {
            throw new RuntimeException("Unable to process encrypted document",
                ex);
        }

    }

    private XlsxDecryptor() {
    }

}

E para criação:

public class XlsxEncryptor {

    public static void encrypt(InputStream input, OutputStream output, 
            String password) throws IOException {

        try {
            POIFSFileSystem fs = new POIFSFileSystem();
            EncryptionInfo info = new EncryptionInfo(fs, EncryptionMode.agile);

            Encryptor enc = info.getEncryptor();
            enc.confirmPassword(password);

            OPCPackage opc = OPCPackage.open(input);
            OutputStream os = enc.getDataStream(fs);
            opc.save(os);
            opc.close();

            fs.writeFilesystem(output);
            output.close();
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        } catch (InvalidFormatException e) {
            throw new RuntimeException(e);
        }

    }

}

Proteção de partes de um documento

O POI implementa a proteção de uma planilha de um documento Excel e a proteção de células. Testei apenas a proteção de planilhas.

A implementação é muito simples, basta usar o método protectSheet. Veja o exemplo para o formato XLSX:

XSSFSheet sheet = workbook.createSheet();
sheet.protectSheet("54321");

E agora o equivalente para o formato XLS:

HSSFSheet sheet = workbook.createSheet();
sheet.protectSheet("54321");

Testes e exemplo

Antes de encerrar o artigo, vou acrescentar aqui dois dos testes unitários que implementei de forma que possam servir como exemplo de uso do POI e do meu projeto.

O primeiro cria um novo arquivo XLSX cripgrafado e em seguida lê o mesmo arquivo verificando se o valor foi gravado corretamente:

@Test
public void createXLSXOpenAndModifyProtected() throws IOException {
    System.out.println("createXLSXOpenAndModifyProtected");

    //creates sheet
    XSSFWorkbook workbook = new XSSFWorkbook();
    XSSFSheet sheet = workbook.createSheet();
    sheet.protectSheet("54321");

    XSSFRow row = sheet.createRow(0);
    XSSFCell cell = row.createCell(0);
    cell.setCellValue("Gravado");

    //saves sheet
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    workbook.write(bos);
    bos.close();
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());

    new File("target/.output-file/xlsx").mkdirs();
    WorksheetEncryptor.encrypt(
        bis, 
        new FileOutputStream("target/.output-file/xlsx/OpenAndModifyProtected.xlsx"), 
        DocumentType.XML, 
        "54321");
    bis.close();

    //read again and check
    XSSFWorkbook workbook2 = (XSSFWorkbook) WorksheetDecryptor.decrypt(
        new FileInputStream("target/.output-file/xlsx/OpenProtected.xlsx"),
        DocumentType.XML, 
        "54321");
    Assert.assertEquals("Gravado", workbook2.getSheetAt(0).getRow(0).getCell(0).getStringCellValue());
}

Em seguida, o segundo lê um arquivo XLS criptografado e verifica o valor da célula:

@Test
public void testOpenAndModifyProtectedXLS() throws IOException {
    System.out.println("### testOpenAndModifyProtectedXLS");
    HSSFWorkbook workbook = (HSSFWorkbook) WorksheetDecryptor.decrypt(
        getClass().getResourceAsStream("xls/OpenAndModifyProtected.xls"),
        DocumentType.LEGACY, 
        "12345");
    String conteudoCelula = workbook.getSheetAt(0).getRow(0).getCell(0)
        .getStringCellValue();
    Assert.assertEquals("Teste criptografia", conteudoCelula);
}

Código-fonte

Todo o código, inclusive os testes, está disponível em um projeto no meu GitHub.

Com o Maven você será capaz de executá-lo sem nenhuma configuração adicional. Basta clonar o repositório.