diff --git a/fastods-crypto/pom.xml b/fastods-crypto/pom.xml index 87a9c28d..32caeec5 100644 --- a/fastods-crypto/pom.xml +++ b/fastods-crypto/pom.xml @@ -74,6 +74,17 @@ easymock test + + com.github.jferard + fastods-testlib + test + + + com.github.jferard + fastods-testlib + 0.8.2-SNAPSHOT + test + diff --git a/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ProtectionFactory.java b/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ProtectionFactory.java index e87cc0b2..fa03af15 100644 --- a/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ProtectionFactory.java +++ b/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ProtectionFactory.java @@ -36,6 +36,9 @@ * 19.698: "Producers should use http://www.w3.org/2000/09/xmldsig#sha256." */ public class ProtectionFactory { + /** Do not instantiate */ + private ProtectionFactory() {} + /** * Create a new protection. **Beware: for security reasons, this fills the password array * with 0's** diff --git a/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/Util.java b/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/Util.java index aa9edc07..941a0a36 100644 --- a/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/Util.java +++ b/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/Util.java @@ -24,7 +24,6 @@ package com.github.jferard.fastods.crypto; -import com.github.jferard.fastods.util.CharsetUtil; import org.bouncycastle.util.encoders.Base64; import java.nio.ByteBuffer; @@ -35,6 +34,9 @@ import java.util.Arrays; public class Util { + /** Do not instantiate */ + private Util() {} + /** * Convert a char array to a byte array containing the SHA-256 digest. **Beware: for security * reasons, this fills the password array with 0's**. diff --git a/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriter.java b/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriter.java index d622290c..8dda9a26 100644 --- a/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriter.java +++ b/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriter.java @@ -26,7 +26,6 @@ import com.github.jferard.fastods.annotation.Beta; import com.github.jferard.fastods.odselement.OdsEntry; -import com.github.jferard.fastods.util.CharsetUtil; import com.github.jferard.fastods.util.ZipUTF8Writer; import com.github.jferard.fastods.util.ZipUTF8WriterBuilder; import org.bouncycastle.util.encoders.Base64; @@ -51,6 +50,7 @@ public class ZipUTF8CryptoWriter implements ZipUTF8Writer { /** * **Beware: for security reasons, this fills the password array with 0's** + * * @param password the password to encrypt data * @return a builder * @throws NoSuchAlgorithmException won't happen since SHA-256 is pretty common @@ -62,11 +62,11 @@ public static ZipUTF8WriterBuilder builder(final char[] password) private final ZipUTF8Writer zipUTF8Writer; private final StandardEncrypter encrypter; + private final byte[] hashedPassword; private ByteArrayOutputStream out; private Writer writer; private OdsEntry curEntry; private boolean toRegister; - private final byte[] hashedPassword; public ZipUTF8CryptoWriter(final ZipUTF8Writer zipUTF8Writer, final StandardEncrypter encrypter, final byte[] hashedPassword) { @@ -81,7 +81,7 @@ public void setComment(final String comment) { } @Override - public void putAndRegisterNextEntry(final OdsEntry entry) throws IOException { + public void putAndRegisterNextEntry(final OdsEntry entry) { this.toRegister = true; this.putNextEntry(entry); } @@ -124,17 +124,9 @@ private EntryAndData getEncryptedEntryAndData(final byte[] plainTextBytes) throw final EntryAndData entryAndData; try { entryAndData = this.getEncryptedEntryAndDataUnchecked(plainTextBytes); - } catch (final NoSuchAlgorithmException e) { - throw new IOException("Can't encrypt file", e); - } catch (final InvalidKeyException e) { - throw new IOException("Can't encrypt file", e); - } catch (final InvalidAlgorithmParameterException e) { - throw new IOException("Can't encrypt file", e); - } catch (final NoSuchPaddingException e) { - throw new IOException("Can't encrypt file", e); - } catch (final BadPaddingException e) { - throw new IOException("Can't encrypt file", e); - } catch (final IllegalBlockSizeException e) { + } catch (final NoSuchAlgorithmException | InvalidKeyException + | InvalidAlgorithmParameterException | NoSuchPaddingException + | BadPaddingException | IllegalBlockSizeException e) { throw new IOException("Can't encrypt file", e); } return entryAndData; @@ -154,7 +146,8 @@ private EntryAndData getEncryptedEntryAndDataUnchecked(final byte[] plainTextByt final long crc32 = this.getCrc32(compressedThenEncryptedData); final OdsEntry entry = this.curEntry.encryptParameters( this.encrypter.buildParameters( - plainTextBytes.length, compressedThenEncryptedData.length, crc32, compressedCheckSum, + plainTextBytes.length, compressedThenEncryptedData.length, crc32, + compressedCheckSum, Base64.toBase64String(salt), Base64.toBase64String(iv))); return new EntryAndData(entry, compressedThenEncryptedData); } diff --git a/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriterBuilder.java b/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriterBuilder.java index 47ec8b22..d26fe951 100644 --- a/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriterBuilder.java +++ b/fastods-crypto/src/main/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriterBuilder.java @@ -41,6 +41,9 @@ * A builder for `ZipUTF8CryptoWriter`. */ public class ZipUTF8CryptoWriterBuilder implements ZipUTF8WriterBuilder { + public static final String AES_CBC_ISO_10126_PADDING = "AES/CBC/ISO10126Padding"; + public static final String SHA_1_PRNG = "SHA1PRNG"; + /** * **Beware: for security reasons, this fills the password array with 0's** * @@ -76,15 +79,13 @@ public ZipUTF8CryptoWriterBuilder(final ZipUTF8WriterBuilderImpl writerBuilder, } @Override - public ZipUTF8Writer build(final OutputStream outputStream) { + public ZipUTF8CryptoWriter build(final OutputStream outputStream) { try { return new ZipUTF8CryptoWriter(this.writerBuilder.build(outputStream), - new StandardEncrypter(SecureRandom.getInstance("SHA1PRNG"), - Cipher.getInstance("AES/CBC/ISO10126Padding"), 100000, 32, 32, this.parametersBuilder + new StandardEncrypter(SecureRandom.getInstance(SHA_1_PRNG), + Cipher.getInstance(AES_CBC_ISO_10126_PADDING), 100000, 32, 32, this.parametersBuilder ), this.hashedPassword); - } catch (final NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } catch (final NoSuchPaddingException e) { + } catch (final NoSuchAlgorithmException | NoSuchPaddingException e) { throw new RuntimeException(e); } } diff --git a/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/ProtectionFactoryTest.java b/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/ProtectionFactoryTest.java new file mode 100644 index 00000000..e8c79682 --- /dev/null +++ b/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/ProtectionFactoryTest.java @@ -0,0 +1,47 @@ +/* + * FastODS - A very fast and lightweight (no dependency) library for creating ODS + * (Open Document Spreadsheet, mainly for Calc) files in Java. + * It's a Martin Schulz's SimpleODS fork + * Copyright (C) 2016-2020 J. FĂ©rard + * SimpleODS - A lightweight java library to create simple OpenOffice spreadsheets + * Copyright (C) 2008-2013 Martin Schulz + * + * This file is part of FastODS. + * + * FastODS is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later + * version. + * + * FastODS is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.github.jferard.fastods.crypto; + +import com.github.jferard.fastods.util.Protection; +import com.github.jferard.fastods.util.XMLUtil; +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; + +public class ProtectionFactoryTest { + @Test + public void test() throws NoSuchAlgorithmException, IOException { + final Protection p = ProtectionFactory.createSha256("passwd".toCharArray()); + final StringBuilder sb = new StringBuilder(); + p.appendAttributes(XMLUtil.create(), sb); + + Assert.assertEquals( + " table:protected=\"true\" table:protection-key=\"DWvmmyZHF/LdM2UuISsXMQS0pke3wRrnLpiF8RzTEvs=\" table:protection-key-digest-algorithm=\"http://www.w3.org/2000/09/xmldsig#sha256\"", + sb.toString()); + } + +} \ No newline at end of file diff --git a/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/StandardEncrypterTest.java b/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/StandardEncrypterTest.java index 0f05a5d0..86e0cddd 100644 --- a/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/StandardEncrypterTest.java +++ b/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/StandardEncrypterTest.java @@ -24,8 +24,11 @@ package com.github.jferard.fastods.crypto; +import com.github.jferard.fastods.XMLConvertible; import com.github.jferard.fastods.odselement.EncryptParameters; +import com.github.jferard.fastods.testlib.DomTester; import com.github.jferard.fastods.util.CharsetUtil; +import com.github.jferard.fastods.util.XMLUtil; import org.bouncycastle.util.encoders.Base64; import org.easymock.EasyMock; import org.junit.Assert; @@ -112,4 +115,57 @@ public void testIVandSalt() throws NoSuchPaddingException, NoSuchAlgorithmExcept PowerMock.verifyAll(); } + + @Test + public void testBuildParameters() + throws NoSuchPaddingException, NoSuchAlgorithmException, IOException { + final SecureRandom sr = PowerMock.createMock(SecureRandom.class); + final StandardEncrypter encrypter = new StandardEncrypter( + sr, Cipher.getInstance("AES/CBC/ISO10126Padding"), + 100000, 32, 32, EncryptParameters.builder() + ); + + PowerMock.resetAll(); + + PowerMock.replayAll(); + final EncryptParameters parameters = + encrypter.buildParameters(100, 10, 40L, "ccs", "salt", "vec"); + + PowerMock.verifyAll(); + Assert.assertEquals(100, parameters.getPlainDataSize()); + Assert.assertEquals(10, parameters.getCompressedThenEncryptedDataSize()); + Assert.assertEquals(40L, parameters.getCrc32()); + + StandardEncrypterTest.assertXMLEquals("" + + "" + + "" + + "" + + "", parameters); + } + + public static String toXML(final XMLConvertible o) throws IOException { + final StringBuilder sb = new StringBuilder(); + o.appendXMLContent(XMLUtil.create(), sb); + return sb.toString(); + } + + /** + * Beware: sensitive to spaces (spaces are children) + * + * @param xml the xml string + * @param o an object convertible to XML + * @throws IOException if an I/O error occurs + */ + public static void assertXMLEquals(final String xml, final XMLConvertible o) + throws IOException { + DomTester.assertEquals(xml, StandardEncrypterTest.toXML(o)); + } } \ No newline at end of file diff --git a/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriterTest.java b/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriterTest.java index 91918a3b..5e6a509e 100644 --- a/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriterTest.java +++ b/fastods-crypto/src/test/java/com/github/jferard/fastods/crypto/ZipUTF8CryptoWriterTest.java @@ -25,6 +25,7 @@ package com.github.jferard.fastods.crypto; import com.github.jferard.fastods.odselement.EncryptParameters; +import com.github.jferard.fastods.odselement.EncryptParametersBuilder; import com.github.jferard.fastods.odselement.StandardOdsEntry; import com.github.jferard.fastods.odselement.UnregisteredOdsEntry; import com.github.jferard.fastods.odselement.UnregisteredStoredEntry; @@ -314,4 +315,74 @@ public void testAppend() throws IOException, NoSuchPaddingException, InvalidKeyE // EOCD 'P', 'K', 5, 6, 0, 0, 0, 0, 2, 0, 2, 0, 117, 0, 0, 0, 6, 1, 0, 0, 0, 0}, bytes); } -} \ No newline at end of file + + @Test + public void testBuilder() + throws NoSuchAlgorithmException { + final ZipUTF8WriterBuilderImpl writerBuilder = + PowerMock.createMock(ZipUTF8WriterBuilderImpl.class); + final EncryptParametersBuilder parametersBuilder = + PowerMock.createMock(EncryptParametersBuilder.class); + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + PowerMock.resetAll(); + EasyMock.expect(writerBuilder.build(bos)).andReturn(null); + + PowerMock.replayAll(); + final ZipUTF8CryptoWriterBuilder builder = + new ZipUTF8CryptoWriterBuilder(writerBuilder, parametersBuilder, + "passwd".toCharArray()); + builder.build(bos); + + PowerMock.verifyAll(); + } + + @Test + public void testExc() + throws IOException, InvalidAlgorithmParameterException, NoSuchPaddingException, + IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, + InvalidKeyException { + final StandardEncrypter encrypter = PowerMock.createMock(StandardEncrypter.class); + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + final ZipUTF8Writer writer = PowerMock.createMock(ZipUTF8Writer.class); + final byte[] salt = "foo".getBytes(StandardCharsets.UTF_8); + final byte[] iv = "bar".getBytes(StandardCharsets.UTF_8); + final byte[] data = "baz".getBytes(StandardCharsets.UTF_8); + final byte[] password = "passwd".getBytes(StandardCharsets.UTF_8); + + PowerMock.resetAll(); + EasyMock.expect(encrypter.generateSalt()).andReturn(salt); + EasyMock.expect(encrypter.generateIV()).andReturn(iv); + EasyMock.expect(encrypter.compress(new byte[0])).andReturn(data); + EasyMock.expect(encrypter.encrypt(data, password, salt, iv)) + .andThrow(new NoSuchAlgorithmException()); + + PowerMock.replayAll(); + final ZipUTF8CryptoWriter cryptoWriter = + new ZipUTF8CryptoWriter(writer, encrypter, password); + cryptoWriter.putNextEntry(new UnregisteredOdsEntry("path")); + Assert.assertThrows(IOException.class, () -> cryptoWriter.closeEntry()); + + PowerMock.verifyAll(); + } + + @Test + public void testSetComment() { + final StandardEncrypter encrypter = PowerMock.createMock(StandardEncrypter.class); + final ByteArrayOutputStream bos = new ByteArrayOutputStream(); + final ZipUTF8Writer writer = PowerMock.createMock(ZipUTF8Writer.class); + final byte[] password = "passwd".getBytes(StandardCharsets.UTF_8); + final StandardOdsEntry entry = new StandardOdsEntry("path", "mt", "v"); + + PowerMock.resetAll(); + writer.setComment("Comment"); + writer.registerEntry(entry); + + PowerMock.replayAll(); + final ZipUTF8CryptoWriter cryptoWriter = + new ZipUTF8CryptoWriter(writer, encrypter, password); + cryptoWriter.setComment("Comment"); + cryptoWriter.registerEntry(entry); + + PowerMock.verifyAll(); + }} \ No newline at end of file