Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,21 @@ protected excel documents, but it can be realized by combining `poi` and `poi-oo
This test class is a reference implementation :
[EncryptionTest](./e2e/src/test/java/org/dhatim/fastexcel/EncryptionTest.java)

### Protect a worksheet from viewing

A worksheet can be hidden and the workbook structure can be protected with a password using `protectWithViewPassword`.

```java
try (OutputStream os = new FileOutputStream("protected.xlsx");
Workbook wb = new Workbook(os, "Application", "1.0")) {

Worksheet ws = wb.newWorksheet("SecretSheet");
ws.value(0, 0, "Sensitive Data");

ws.protectWithViewPassword("viewPassword");
}
```

This hides the worksheet and protects the workbook structure, so users cannot unhide, move, rename, or delete sheets without the workbook structure password.

Note: this is different from `protect(...)`, which protects a worksheet from editing. `protectWithViewPassword(...)` is intended to restrict viewing by hiding the worksheet and protecting the workbook structure.
124 changes: 124 additions & 0 deletions e2e/src/test/java/org/dhatim/fastexcel/ViewPasswordE2ETest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.dhatim.fastexcel;

import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.dhatim.fastexcel.reader.ReadableWorkbook;
import org.dhatim.fastexcel.reader.Row;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertTrue;

class ViewPasswordE2ETest {

@Test
void testSheetIsHiddenAfterProtectWithViewPassword() throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (Workbook wb = new Workbook(os, "Test", "1.0")) {
Worksheet ws = wb.newWorksheet("SecretSheet");
ws.value(0, 0, "Sensitive Data");
ws.value(1, 0, "More Sensitive Data");
ws.protectWithViewPassword("viewPassword");
}

byte[] bytes = os.toByteArray();

// Verify sheet is hidden via Apache POI
try (XSSFWorkbook poiWb = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
assertTrue(poiWb.isStructureLocked());
}
}

@Test
void testWorkbookStructureIsLockedAfterProtectWithViewPassword() throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (Workbook wb = new Workbook(os, "Test", "1.0")) {
Worksheet ws = wb.newWorksheet("SecretSheet");
ws.value(0, 0, "Sensitive Data");
ws.protectWithViewPassword("viewPassword");
}

byte[] bytes = os.toByteArray();

// Verify workbook structure is locked via Apache POI
try (XSSFWorkbook poiWb = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
assertThat(poiWb.isStructureLocked()).isTrue();
}
}

@Test
void testDataIsPreservedAfterProtectWithViewPassword() throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (Workbook wb = new Workbook(os, "Test", "1.0")) {
Worksheet ws = wb.newWorksheet("SecretSheet");
ws.value(0, 0, "Sensitive Data");
ws.value(1, 0, "More Sensitive Data");
ws.protectWithViewPassword("viewPassword");
}

byte[] bytes = os.toByteArray();

// Verify data is still readable via fastexcel reader
try (ReadableWorkbook rwb = new ReadableWorkbook(new ByteArrayInputStream(bytes))) {
try (Stream<Row> rows = rwb.getFirstSheet().openStream()) {
List<String> values = rows
.map(r -> r.getCellAsString(0).orElse(""))
.collect(Collectors.toList());
assertThat(values).containsExactly("Sensitive Data", "More Sensitive Data");
}
}
}

@Test
void testOnlyProtectedSheetIsHidden() throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (Workbook wb = new Workbook(os, "Test", "1.0")) {
Worksheet secretSheet = wb.newWorksheet("SecretSheet");
secretSheet.value(0, 0, "Sensitive Data");
secretSheet.protectWithViewPassword("viewPassword");

// Add a second visible sheet
Worksheet publicSheet = wb.newWorksheet("PublicSheet");
publicSheet.value(0, 0, "Public Data");
}

byte[] bytes = os.toByteArray();

try (XSSFWorkbook poiWb = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
// First sheet (SecretSheet) should be hidden
assertThat(poiWb.isSheetHidden(0)).isTrue();
// Second sheet (PublicSheet) should be visible
assertThat(poiWb.isSheetHidden(1)).isFalse();
}
}

@Test
void testProtectWithViewPasswordAndEditPassword() throws IOException {
ByteArrayOutputStream os = new ByteArrayOutputStream();
try (Workbook wb = new Workbook(os, "Test", "1.0")) {
Worksheet ws = wb.newWorksheet("SecretSheet");
ws.value(0, 0, "Sensitive Data");
// Protect viewing
ws.protectWithViewPassword("viewPassword");
// Also protect editing
ws.protect("editPassword");
}

byte[] bytes = os.toByteArray();

try (XSSFWorkbook poiWb = new XSSFWorkbook(new ByteArrayInputStream(bytes))) {
// Sheet should be hidden
assertThat(poiWb.isSheetHidden(0)).isTrue();
// Workbook structure should be locked
assertThat(poiWb.isStructureLocked()).isTrue();
// Sheet should be protected
assertThat(poiWb.getSheetAt(0).getProtect()).isTrue();
}
}
}
92 changes: 92 additions & 0 deletions e2e/src/test/java/org/dhatim/fastexcel/WorkbookProtectionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package org.dhatim.fastexcel;

import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;

import java.io.*;

import static org.junit.jupiter.api.Assertions.*;

public class WorkbookProtectionTest {

private static final File testFile = new File("target/workbookProtectionTest.xlsx");

private static final String testPassword = "myPassword";

private static final String testContent = "Hello fastexcel";

// ── Write helpers ────────────────────────────────────────────────────────

void fastexcelWriteWithStructureProtection() throws IOException {
try (FileOutputStream fos = new FileOutputStream(testFile);
Workbook wb = new Workbook(fos, "Test", "1.0")) {
Worksheet ws = wb.newWorksheet("Sheet1");
ws.value(0, 0, testContent);
wb.protectStructure(testPassword);
}
}

void fastexcelWriteWithoutStructureProtection() throws IOException {
try (FileOutputStream fos = new FileOutputStream(testFile);
Workbook wb = new Workbook(fos, "Test", "1.0")) {
Worksheet ws = wb.newWorksheet("Sheet1");
ws.value(0, 0, testContent);
}
}

void fastexcelWriteWithNullPassword() throws IOException {
try (FileOutputStream fos = new FileOutputStream(testFile);
Workbook wb = new Workbook(fos, "Test", "1.0")) {
Worksheet ws = wb.newWorksheet("Sheet1");
ws.value(0, 0, testContent);
wb.protectStructure(testPassword); // set password
wb.protectStructure(null); // then remove it
}
}

// ── Read helpers ─────────────────────────────────────────────────────────

void poiVerifyStructureIsLocked() throws IOException {
try (FileInputStream fis = new FileInputStream(testFile);
XSSFWorkbook poiWb = new XSSFWorkbook(fis)) {
assertTrue(poiWb.isStructureLocked(),
"Workbook structure should be locked");
}
}

void poiVerifyStructureIsNotLocked() throws IOException {
try (FileInputStream fis = new FileInputStream(testFile);
XSSFWorkbook poiWb = new XSSFWorkbook(fis)) {
assertFalse(poiWb.isStructureLocked(),
"Workbook structure should not be locked");
}
}

// ── Cleanup ──────────────────────────────────────────────────────────────

@AfterAll
static void cleanup() {
testFile.delete();
}

// ── Tests ────────────────────────────────────────────────────────────────

@Test
void fastexcelWrite_poiVerifyStructureLocked() throws Exception {
fastexcelWriteWithStructureProtection();
poiVerifyStructureIsLocked();
}

@Test
void fastexcelWrite_poiVerifyStructureNotLocked() throws Exception {
fastexcelWriteWithoutStructureProtection();
poiVerifyStructureIsNotLocked();
}

@Test
void fastexcelWrite_nullPassword_poiVerifyStructureNotLocked() throws Exception {
fastexcelWriteWithNullPassword();
poiVerifyStructureIsNotLocked();
}
}
2 changes: 1 addition & 1 deletion fastexcel-reader/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
module org.dhatim.fastexcel.reader {
requires java.xml;
requires java.logging;
requires org.apache.commons.compress;
requires com.fasterxml.aalto;
requires java.logging;
exports org.dhatim.fastexcel.reader;
}
61 changes: 52 additions & 9 deletions fastexcel-writer/src/main/java/org/dhatim/fastexcel/Workbook.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class Workbook implements Closeable {

private int activeTab = 0;
private boolean finished = false;
private String workbookPasswordHash;
private final String applicationName;
private final String applicationVersion;
private final List<Worksheet> worksheets = new ArrayList<>();
Expand Down Expand Up @@ -92,6 +93,41 @@ public void setCompressionLevel(int level) {
public void setActiveTab(int tabIndex) {
this.activeTab = tabIndex;
}
/**
* Protects the workbook structure with a password.
* Prevents users from unhiding, adding, moving, or deleting sheets.
* (Note that this is not very secure and only meant for discouraging changes. Same amount of
* protection as the edit password for worksheets.)
* @param password The password to use.
*/
public void protectStructure(String password) {
this.workbookPasswordHash = password != null ? hashPassword(password) : null;
}

/**
* Hash the password using the same algorithm as worksheet protection.
* @param password The password to hash.
* @return The password hash as a hex string.
*/
private static String hashPassword(String password) {
byte[] passwordCharacters = password.getBytes();
int hash = 0;
if (passwordCharacters.length > 0) {
int charIndex = passwordCharacters.length;
while (charIndex-- > 0) {
hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff);
hash ^= passwordCharacters[charIndex];
}
hash = ((hash >> 14) & 0x01) | ((hash << 1) & 0x7fff);
hash ^= passwordCharacters.length;
hash ^= (0x8000 | ('N' << 8) | 'K');
}
return Integer.toHexString(hash & 0xffff);
}





public void setGlobalDefaultFont(String fontName, double fontSize) {
this.setGlobalDefaultFont(Font.build(null, null, null, fontName, BigDecimal.valueOf(fontSize), null, null));
Expand Down Expand Up @@ -313,15 +349,22 @@ private Set<ImageType> collectUsedImageTypes() {
*/
private void writeWorkbookFile() throws IOException {
writeFile("xl/workbook.xml", w -> {
w.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<workbook " +
"xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " +
"xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
"<workbookPr date1904=\"false\"/>" +
"<bookViews>" +
"<workbookView activeTab=\"" + activeTab + "\"/>" +
"</bookViews>" +
"<sheets>");
w.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<workbook " +
"xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" " +
"xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">" +
"<workbookPr date1904=\"false\"/>");

if (workbookPasswordHash != null) {
w.append("<workbookProtection workbookPassword=\"")
.append(workbookPasswordHash)
.append("\" lockStructure=\"1\"/>");
}

w.append("<bookViews>" +
"<workbookView activeTab=\"" + activeTab + "\"/>" +
"</bookViews>" +
"<sheets>");

for (Worksheet ws : worksheets) {
writeWorkbookSheet(w, ws);
Expand Down
12 changes: 12 additions & 0 deletions fastexcel-writer/src/main/java/org/dhatim/fastexcel/Worksheet.java
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,18 @@ public void protect(String password, Set<SheetProtectionOption> options) {
this.sheetProtectionOptions = options;
this.passwordHash = hashPassword(password);
}
/**
* Protects the sheet from viewing by hiding it and locking
* the workbook structure with a password.
* Unauthorized users will not be able to unhide the sheet
* without the correct password.
* (Note that this is not very secure and only meant for discouraging changes.)
* @param password The password required to unhide the sheet.
*/
public void protectWithViewPassword(String password) {
this.setVisibilityState(VisibilityState.HIDDEN);
this.workbook.protectStructure(password);
}

/**
* Applies autofilter specifically to the given cell range
Expand Down
Loading