Skip to content

Commit

Permalink
Password protection of sheets, bump to 1.5.0.
Browse files Browse the repository at this point in the history
  • Loading branch information
Salvo Isaja committed Jan 31, 2022
1 parent 1db4c0a commit a639147
Show file tree
Hide file tree
Showing 12 changed files with 315 additions and 20 deletions.
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
LargeXlsx - Minimalistic .net library to write large XLSX files
Copyright 2020-2021 Salvatore ISAJA
Copyright 2020-2022 Salvatore ISAJA

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

Expand Down
50 changes: 44 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

This is a minimalistic yet feature-rich library, written in C# targeting .net standard 2.0, providing simple primitives to write Excel files in XLSX format in a streamed manner, so that potentially huge files can be created while consuming a low, constant amount of memory.

Starting from version 0.0.9 this library does not rely any longer on Microsoft's [Office Open XML library](https://github.com/OfficeDev/Open-XML-SDK), but writes XLSX files directly. This solves a memory consumption problem on .net core (caused by an [issue on System.IO.Packaging](https://github.com/dotnet/corefx/issues/24457) used by the Open XML SDK to write XLSX's zip packages) and provides further performance boost.


## Supported features

Expand All @@ -20,6 +18,7 @@ Currently the library supports:
* auto filter
* cell validation, such as dropdown list of allowed values
* right-to-left worksheets, to support languages such as Arabic and Hebrew
* password protection of sheets against accidental modification


## Example
Expand Down Expand Up @@ -79,10 +78,13 @@ The output is like:

## Changelog

* 1.3: Optional ZIP64 support for really huge files.
* 1.2: Right-to-left worksheets. Parametric compression level to trade between space and speed.
* 1.1: Number format for text-formatted cells (the "@" formatting).
* 1.0: Finalized API.
* 1.5: Password protection of sheets
* 1.4: Support for font underline, thanks to [Sergey Bochkin](https://github.com/sbochkin)
* 1.3: Optional ZIP64 support for really huge files
* 1.2: Right-to-left worksheets. Parametric compression level to trade between space and speed
* 1.1: Number format for text-formatted cells (the "@" formatting)
* 1.0: Finalized API
* 0.0.9: Started writing XLSX files directly rather than using Microsoft's [Office Open XML library](https://github.com/OfficeDev/Open-XML-SDK)

## Usage

Expand Down Expand Up @@ -330,6 +332,42 @@ Note that, due to the internals of the XLSX file format, all validation objects
This means that you may call `AddDataValidation` at any moment while you are writing a worksheet (that is between a `BeginWorksheet` and the next one, or disposal of the `XlsxWriter` object), even for cells already written or well before writing them, or cells you won't write content to.


### Password protection of sheets

Password protection of worksheets helps preventing accidental modification of data. You can enable protection on the worksheet being written using:

```csharp
// class XlsxWriter
public XlsxWriter SetSheetProtection(XlsxSheetProtection sheetProtection);

// class XlsxSheetProtection
public XlsxSheetProtection(
string password,
bool sheet = true,
bool objects = true,
bool scenarios = true,
bool formatCells = true,
bool formatColumns = true,
bool formatRows = true,
bool insertColumns = true,
bool insertRows = true,
bool insertHyperlinks = true,
bool deleteColumns = true,
bool deleteRows = true,
bool selectLockedCells = false,
bool sort = true,
bool autoFilter = true,
bool pivotTables = true,
bool selectUnlockedCells = false);
```

Each flag in `XlsxSheetProtection` specifies what operations are **protected**, that is not allowed. By default, only selecting cells is allowed. The password must have a length between 1 and 255 characters.

You can call `SetSheetProtection` at any moment while writing a worksheet (that is between a `BeginWorksheet` and the next one, or disposal of the `XlsxWriter` object). Each worksheet can contain only up to one protection definition, thus if you call `SetSheetProtection` multiple times for the same worksheet only the last one will apply.

**Note:** password protection of sheets is not to be confused with workbook encryption and is not meant to be secure. File contents are still written in clear text and may be changed by deliberately editing the file. The password is not written into the file but a hash of the password is.


### Styling

Styling lets you apply colors or other formatting to cells being written. A style is made up of five components:
Expand Down
3 changes: 2 additions & 1 deletion examples/Examples/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
LargeXlsx - Minimalistic .net library to write large XLSX files
Copyright 2020-2021 Salvatore ISAJA. All rights reserved.
Copyright 2020-2022 Salvatore ISAJA. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand Down Expand Up @@ -40,6 +40,7 @@ public static void Main(string[] args)
DataValidation.Run();
RightToLeft.Run();
Zip64Small.Run();
SheetProtection.Run();
Large.Run();
StyledLarge.Run();
StyledLargeCreateStyles.Run();
Expand Down
48 changes: 48 additions & 0 deletions examples/Examples/SheetProtection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
LargeXlsx - Minimalistic .net library to write large XLSX files
Copyright 2020-2022 Salvatore ISAJA. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System.IO;
using LargeXlsx;

namespace Examples
{
public static class SheetProtection
{
public static void Run()
{
using var stream = new FileStream($"{nameof(SheetProtection)}.xlsx", FileMode.Create, FileAccess.Write);
using var xlsxWriter = new XlsxWriter(stream);
xlsxWriter
.BeginWorksheet("Sheet 1")
.SetSheetProtection(new XlsxSheetProtection("Lorem ipsum", autoFilter: false))
.BeginRow().Write("A1").Write("B1")
.BeginRow().Write("A2").Write("B2")
.BeginRow().Write("A3").Write("B3")
.SetAutoFilter(1, 1, xlsxWriter.CurrentRowNumber - 1, 2);
}
}
}
2 changes: 1 addition & 1 deletion examples/Examples/Simple.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
LargeXlsx - Minimalistic .net library to write large XLSX files
Copyright 2020 Salvatore ISAJA. All rights reserved.
Copyright 2020-2022 Salvatore ISAJA. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand Down
4 changes: 2 additions & 2 deletions src/LargeXlsx/LargeXlsx.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>1.4.0</Version>
<Version>1.5.0</Version>
<Authors>Salvo Isaja</Authors>
<Company>Salvo Isaja</Company>
<Copyright>Copyright 2020-2021 Salvatore Isaja</Copyright>
<Copyright>Copyright 2020-2022 Salvatore Isaja</Copyright>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageProjectUrl>https://github.com/salvois/LargeXlsx</PackageProjectUrl>
<Description>Minimalistic .net library to write large Excel files in XLSX format with low memory consumption using streamed write.</Description>
Expand Down
19 changes: 18 additions & 1 deletion src/LargeXlsx/Util.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
LargeXlsx - Minimalistic .net library to write large XLSX files
Copyright 2020 Salvatore ISAJA. All rights reserved.
Copyright 2020-2022 Salvatore ISAJA. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand All @@ -25,6 +25,8 @@ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace LargeXlsx
Expand Down Expand Up @@ -100,5 +102,20 @@ public static string EnumToAttributeValue<T>(T enumValue)
var s = enumValue.ToString();
return char.ToLowerInvariant(s[0]) + s.Substring(1);
}

// Hashing procedure courtesy of https://docs.microsoft.com/en-us/openspecs/office_file_formats/ms-offcrypto/1357ea58-646e-4483-92ef-95d718079d6f
public static byte[] ComputePasswordHash(string password, byte[] saltValue, int spinCount)
{
var hasher = new SHA512Managed();
var hash = hasher.ComputeHash(saltValue.Concat(Encoding.Unicode.GetBytes(password)).ToArray());
for (var i = 0; i < spinCount; i++)
{
var iterator = BitConverter.GetBytes(i);
if (!BitConverter.IsLittleEndian)
Array.Reverse(iterator);
hash = hasher.ComputeHash(hash.Concat(iterator).ToArray());
}
return hash;
}
}
}
48 changes: 43 additions & 5 deletions src/LargeXlsx/Worksheet.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
LargeXlsx - Minimalistic .net library to write large XLSX files
Copyright 2020-2021 Salvatore ISAJA. All rights reserved.
Copyright 2020-2022 Salvatore ISAJA. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Expand Down Expand Up @@ -35,13 +35,16 @@ namespace LargeXlsx
{
internal class Worksheet : IDisposable
{
private const int MinSheetProtectionPasswordLength = 1;
private const int MaxSheetProtectionPasswordLength = 255;
private readonly Stream _stream;
private readonly StreamWriter _streamWriter;
private readonly Stylesheet _stylesheet;
private readonly List<string> _mergedCellRefs;
private readonly Dictionary<XlsxDataValidation, List<string>> _cellRefsByDataValidation;
private string _autoFilterRef;
private string _autoFilterAbsoluteRef;
private XlsxSheetProtection _sheetProtection;

public int Id { get; }
public string Name { get; }
Expand Down Expand Up @@ -76,6 +79,7 @@ public void Dispose()
{
CloseLastRow();
_streamWriter.Write("</sheetData>");
WriteSheetProtection();
WriteAutoFilter();
WriteMergedCells();
WriteDataValidations();
Expand Down Expand Up @@ -184,6 +188,13 @@ public void AddDataValidation(int fromRow, int fromColumn, int rowCount, int col
cellRefs.Add(cellRef);
}

public void SetSheetProtection(XlsxSheetProtection sheetProtection)
{
if (sheetProtection.Password.Length < MinSheetProtectionPasswordLength || sheetProtection.Password.Length > MaxSheetProtectionPasswordLength)
throw new ArgumentException("Invalid password length");
_sheetProtection = sheetProtection;
}

private void EnsureRow()
{
if (CurrentColumnNumber == 0)
Expand Down Expand Up @@ -235,8 +246,8 @@ private void WriteAutoFilter()

private void WriteMergedCells()
{
if (!_mergedCellRefs.Any()) return;

if (!_mergedCellRefs.Any())
return;
_streamWriter.Write("<mergeCells count=\"{0}\">", _mergedCellRefs.Count);
foreach (var mergedCell in _mergedCellRefs)
_streamWriter.Write("<mergeCell ref=\"{0}\"/>", mergedCell);
Expand All @@ -245,8 +256,8 @@ private void WriteMergedCells()

private void WriteDataValidations()
{
if (!_cellRefsByDataValidation.Any()) return;

if (!_cellRefsByDataValidation.Any())
return;
_streamWriter.Write("<dataValidations count=\"{0}\">", _cellRefsByDataValidation.Count);
foreach (var kvp in _cellRefsByDataValidation)
{
Expand All @@ -269,5 +280,32 @@ private void WriteDataValidations()
}
_streamWriter.Write("</dataValidations>");
}

private void WriteSheetProtection()
{
if (_sheetProtection == null)
return;
const int spinCount = 100000;
var saltValue = Guid.NewGuid().ToByteArray();
var hash = Util.ComputePasswordHash(_sheetProtection.Password, saltValue, spinCount);
_streamWriter.Write("<sheetProtection algorithmName=\"SHA-512\" hashValue=\"{0}\" saltValue=\"{1}\" spinCount=\"{2}\"", Convert.ToBase64String(hash), Convert.ToBase64String(saltValue), spinCount);
if (_sheetProtection.Sheet) _streamWriter.Write(" sheet=\"1\"");
if (_sheetProtection.Objects) _streamWriter.Write(" objects=\"1\"");
if (_sheetProtection.Scenarios) _streamWriter.Write(" scenarios=\"1\"");
if (!_sheetProtection.FormatCells) _streamWriter.Write(" formatCells=\"0\"");
if (!_sheetProtection.FormatColumns) _streamWriter.Write(" formatColumns=\"0\"");
if (!_sheetProtection.FormatRows) _streamWriter.Write(" formatRows=\"0\"");
if (!_sheetProtection.InsertColumns) _streamWriter.Write(" insertColumns=\"0\"");
if (!_sheetProtection.InsertRows) _streamWriter.Write(" insertRows=\"0\"");
if (!_sheetProtection.InsertHyperlinks) _streamWriter.Write(" insertHyperlinks=\"0\"");
if (!_sheetProtection.DeleteColumns) _streamWriter.Write(" deleteColumns=\"0\"");
if (!_sheetProtection.DeleteRows) _streamWriter.Write(" deleteRows=\"0\"");
if (_sheetProtection.SelectLockedCells) _streamWriter.Write(" selectLockedCells=\"1\"");
if (!_sheetProtection.Sort) _streamWriter.Write(" sort=\"0\"");
if (!_sheetProtection.AutoFilter) _streamWriter.Write(" autoFilter=\"0\"");
if (!_sheetProtection.PivotTables) _streamWriter.Write(" pivotTables=\"0\"");
if (_sheetProtection.SelectUnlockedCells) _streamWriter.Write(" selectUnlockedCells=\"1\"");
_streamWriter.Write("/>");
}
}
}
Loading

0 comments on commit a639147

Please sign in to comment.