fpspreadsheet: Add writing support for xls password hashes. Add test case for it.

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@5799 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2017-03-07 22:02:09 +00:00
parent 87ecb8241c
commit a4457a850e
12 changed files with 287 additions and 5 deletions

View File

@ -308,6 +308,18 @@ msgstr "Diese Operation bei Index %d überschreitet den Bereich der definierten
msgid "Palette index %d"
msgstr "Palettenindex %d"
#: fpsstrings.rspasswordremoved_biff2
msgid "Password removed (BIFF2 requires matching workbook and worksheet passwords)"
msgstr ""
#: fpsstrings.rspasswordremoved_excel
msgid "Password removed (Hashing algorithm not compatible with Excel)"
msgstr ""
#: fpsstrings.rspasswordremoved_notvalid
msgid "Password removed (Not valid)."
msgstr ""
#: fpsstrings.rspurple
msgid "purple"
msgstr "purpur"
@ -419,3 +431,4 @@ msgstr "Die Datei kann nicht geschrieben werden, weil der Name des Arbeitsblatte
#: fpsstrings.rsyellow
msgid "yellow"
msgstr "gelb"

View File

@ -297,6 +297,18 @@ msgstr ""
msgid "Palette index %d"
msgstr ""
#: fpsstrings.rspasswordremoved_biff2
msgid "Password removed (BIFF2 requires matching workbook and worksheet passwords)"
msgstr ""
#: fpsstrings.rspasswordremoved_excel
msgid "Password removed (Hashing algorithm not compatible with Excel)"
msgstr ""
#: fpsstrings.rspasswordremoved_notvalid
msgid "Password removed (Not valid)."
msgstr ""
#: fpsstrings.rspurple
msgid "purple"
msgstr ""

View File

@ -311,6 +311,18 @@ msgstr "A művelet túllépi a rács sorainak tartományát."
msgid "Palette index %d"
msgstr "Paletta index: %d"
#: fpsstrings.rspasswordremoved_biff2
msgid "Password removed (BIFF2 requires matching workbook and worksheet passwords)"
msgstr ""
#: fpsstrings.rspasswordremoved_excel
msgid "Password removed (Hashing algorithm not compatible with Excel)"
msgstr ""
#: fpsstrings.rspasswordremoved_notvalid
msgid "Password removed (Not valid)."
msgstr ""
#: fpsstrings.rspurple
msgid "purple"
msgstr "bíbor"

View File

@ -297,6 +297,18 @@ msgstr ""
msgid "Palette index %d"
msgstr ""
#: fpsstrings.rspasswordremoved_biff2
msgid "Password removed (BIFF2 requires matching workbook and worksheet passwords)"
msgstr ""
#: fpsstrings.rspasswordremoved_excel
msgid "Password removed (Hashing algorithm not compatible with Excel)"
msgstr ""
#: fpsstrings.rspasswordremoved_notvalid
msgid "Password removed (Not valid)."
msgstr ""
#: fpsstrings.rspurple
msgid "purple"
msgstr ""

View File

@ -301,6 +301,18 @@ msgstr "Эта операция превышает диапазон опреде
msgid "Palette index %d"
msgstr "Индекс палитры %d"
#: fpsstrings.rspasswordremoved_biff2
msgid "Password removed (BIFF2 requires matching workbook and worksheet passwords)"
msgstr ""
#: fpsstrings.rspasswordremoved_excel
msgid "Password removed (Hashing algorithm not compatible with Excel)"
msgstr ""
#: fpsstrings.rspasswordremoved_notvalid
msgid "Password removed (Not valid)."
msgstr ""
#: fpsstrings.rspurple
msgid "purple"
msgstr ""

View File

@ -4298,12 +4298,18 @@ begin
s := GetAttrValue(styleChildNode, 'style:cell-protect');
if s = 'none' then
fmt.Protection := []
else if s = 'hidden-and-protected' then
else if (s = 'protected formula-hidden') or (s = 'formula-hidden protected') then
fmt.Protection := [cpLockCell, cpHideFormulas]
else if s = 'protected' then
fmt.Protection := [cpLockCell]
else if s = 'formula-hidden' then
fmt.Protection := [cpHideFormulas];
fmt.Protection := [cpHideFormulas]
else if s = 'hidden-and-protected' then
fmt.Protection := [cpLockCell, cpHideFormulas];
// NOTE: This not exact... According to
// https://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html,
// section 20.246, this hides and locks cell content, not just
// formulas...
if fmt.Protection <> DEFAULT_CELL_PROTECTION then
Include(fmt.UsedFormattingFields, uffProtection);
end
@ -5359,7 +5365,7 @@ begin
else if (AFormat.Protection *[cpLockCell, cpHideFormulas] = [cpHideFormulas]) then
Result := 'formula-hidden'
else
Result := 'hidden-and-protected'; // or: 'protected formula-hidden'
Result := 'protected formula-hidden'; // or: 'hidden-and-protected'
Result := ' style:cell-protect="' + Result + '"';
end;

View File

@ -177,6 +177,11 @@ resourcestring
rsMultipleSheetsOnlyWithRestorePosition = 'Export to multiple sheets is possible '+
'only if position is restored.';
// Protection
rsPasswordRemoved_BIFF2 = 'Password removed (BIFF2 requires matching workbook '+
'and worksheet passwords)';
rsPasswordRemoved_NotValid = 'Password removed (Not valid).';
rsPasswordRemoved_Excel = 'Password removed (Hashing algorithm not compatible with Excel)';
const
// Color names which do not have to be translated. They will be removed.

View File

@ -63,6 +63,7 @@ type
procedure ReadIXFE(AStream: TStream);
procedure ReadLabel(AStream: TStream); override;
procedure ReadNumber(AStream: TStream); override;
procedure ReadPASSWORD(AStream: TStream);
procedure ReadPROTECT(AStream: TStream);
procedure ReadRowColXF(AStream: TStream; out ARow, ACol: Cardinal; out AXF: Word); override;
procedure ReadRowInfo(AStream: TStream); override;
@ -122,6 +123,7 @@ type
const AValue: string; ACell: PCell); override;
procedure WriteNumber(AStream: TStream; const ARow, ACol: Cardinal;
const AValue: double; ACell: PCell); override;
procedure WritePASSWORD(AStream: TStream);
procedure WriteRow(AStream: TStream; ASheet: TsWorksheet;
ARowIndex, AFirstColIndex, ALastColIndex: Cardinal; ARow: PRow); override;
procedure WriteRPNFormula(AStream: TStream; const ARow, ACol: Cardinal;
@ -621,7 +623,7 @@ begin
INT_EXCEL_ID_NUMBER : ReadNumber(AStream);
INT_EXCEL_ID_PANE : ReadPane(AStream);
INT_EXCEL_ID_OBJECTPROTECT : ReadObjectProtect(AStream);
INT_EXCEL_ID_PASSWORD : ReadPASSWORD(AStream, FWorksheet);
INT_EXCEL_ID_PASSWORD : ReadPASSWORD(AStream);
INT_EXCEL_ID_PRINTGRID : ReadPrintGridLines(AStream);
INT_EXCEL_ID_PRINTHEADERS : ReadPrintHeaders(AStream);
INT_EXCEL_ID_PROTECT : ReadPROTECT(AStream);
@ -871,6 +873,29 @@ begin
FPendingXFIndex := WordLEToN(AStream.ReadWord);
end;
{@@ ----------------------------------------------------------------------------
Reads a PASSWORD record. Since BIFF2 does not have multiple worksheets the
same password is stored in the workbook and worksheet cryptoinfo records.
-------------------------------------------------------------------------------}
procedure TsSpreadBIFF2Reader.ReadPASSWORD(AStream: TStream);
var
hash: Word;
cinfo: TsCryptoInfo;
begin
hash := WordLEToN(AStream.ReadWord);
if hash = 0 then
exit; // no password
InitCryptoInfo(cinfo);
cinfo.PasswordHash := Format('%.4x', [hash]);
cinfo.Algorithm := caExcel;
// Use the same password for workbook and worksheet protection because
// BIFF2 can have only a single sheet.
FWorkbook.CryptoInfo := cinfo;
FWorksheet.CryptoInfo := cinfo;
end;
procedure TsSpreadBIFF2Reader.ReadPROTECT(AStream: TStream);
begin
inherited ReadPROTECT(AStream);
@ -1571,10 +1596,12 @@ begin
WriteFormatCount(AStream);
WriteNumFormats(AStream);
if (bpLockStructure in Workbook.Protection) or FWorksheet.IsProtected then
WritePROTECT(AStream, true);
WriteWindowProtect(AStream, bpLockWindows in Workbook.Protection);
WriteObjectProtect(AStream, FWorksheet);
WritePASSWORD(AStream);
WriteXFRecords(AStream);
WriteDefaultColWidth(AStream, FWorksheet);
@ -2216,6 +2243,64 @@ begin
AStream.WriteBuffer(rec, SizeOf(Rec));
end;
procedure TsSpreadBIFF2Writer.WritePassword(AStream: TStream);
var
hash: Word;
hb, hs: LongInt;
begin
hb := 0;
if (Workbook.CryptoInfo.PasswordHash <> '') and
not TryStrToInt('$' + Workbook.CryptoInfo.PasswordHash, hb) then
begin
Workbook.AddErrorMsg(rsPasswordRemoved_NotValid);
exit;
end;
hs := 0;
if (FWorksheet.CryptoInfo.PasswordHash <> '') and
not TryStrToInt('$' + FWorksheet.CryptoInfo.PasswordHash, hs) then
begin
Workbook.AddErrorMsg(rsPasswordRemoved_NotValid);
exit;
end;
// Neither workbook nor worksheet password set
if (hb = 0) and (hs = 0) then
exit;
// Only workbook password set. Check for Excel algorithm.
if (hb <> 0) and (hs = 0) then begin
if Workbook.CryptoInfo.Algorithm <> caExcel then begin
Workbook.AddErrorMsg(rsPasswordRemoved_Excel);
exit;
end;
hash := hb;
end else
// Only worksheet password set, check for Excel algorithm
if (hs <> 0) and (hb = 0) then begin
if FWorksheet.CryptoInfo.Algorithm <> caExcel then begin
Workbook.AddErrorMsg(rsPasswordRemoved_Excel);
exit;
end;
hash := hs;
end else
if (hs <> hb) then begin
Workbook.AddErrorMsg(rsPasswordRemoved_BIFF2);
exit;
end else
if (Workbook.CryptoInfo.Algorithm <> caExcel) or
(FWorksheet.CryptoInfo.Algorithm <> caExcel) then
begin
Workbook.AddErrorMsg(rsPasswordRemoved_Excel);
exit;
end else
hash := hs; // or hb -- they are equal here.
// Write out record
WriteBIFFHeader(AStream, INT_EXCEL_ID_PASSWORD, 2);
AStream.WriteWord(WordToLE(hash));
end;
procedure TsSpreadBIFF2Writer.WriteRow(AStream: TStream; ASheet: TsWorksheet;
ARowIndex, AFirstColIndex, ALastColIndex: Cardinal; ARow: PRow);
var

View File

@ -1140,6 +1140,7 @@ begin
WriteCODEPAGE(AStream, FCodePage);
WriteWindowProtect(AStream, bpLockWindows in Workbook.Protection);
WritePROTECT(AStream, bpLockStructure in Workbook.Protection);
WritePASSWORD(AStream, Workbook.CryptoInfo);
WriteEXTERNCOUNT(AStream);
WriteEXTERNSHEET(AStream);
WriteDefinedNames(AStream);
@ -1187,11 +1188,15 @@ begin
WriteMargin(AStream, 2); // 2 = top margin
WriteMargin(AStream, 3); // 3 = bottom margin
WritePageSetup(AStream);
// Protection
if FWorksheet.IsProtected then begin
WritePROTECT(AStream, true);
// WriteScenarioProtect(AStream);
WriteObjectProtect(AStream, FWorksheet);
WritePASSWORD(AStream, FWorksheet.CryptoInfo);
end;
WriteDefaultColWidth(AStream, FWorksheet);
WriteColInfos(AStream, FWorksheet);
WriteDimensions(AStream, FWorksheet);

View File

@ -2132,6 +2132,7 @@ begin
WriteCodePage(AStream, 'ucs2le'); // = utf-16
WriteWindowProtect(AStream, bpLockWindows in Workbook.Protection);
WritePROTECT(AStream, bpLockStructure in Workbook.Protection);
WritePASSWORD(AStream, Workbook.CryptoInfo);
WriteWINDOW1(AStream);
WriteFonts(AStream);
WriteNumFormats(AStream);
@ -2179,15 +2180,18 @@ begin
WriteMargin(AStream, 2); // 2 = top margin
WriteMargin(AStream, 3); // 3 = bottom margin
WritePageSetup(AStream);
// Protection
if FWorksheet.IsProtected then begin
WritePROTECT(AStream, true);
// WriteScenarioProtect(AStream);
WriteObjectProtect(AStream, FWorksheet);
WritePASSWORD(AStream, FWorksheet.CryptoInfo);
end;
WriteDefaultColWidth(AStream, FWorksheet);
WriteColInfos(AStream, FWorksheet);
WriteDimensions(AStream, FWorksheet);
//WriteRowAndCellBlock(AStream, sheet);
if (boVirtualMode in Workbook.Options) then
WriteVirtualCells(AStream, FWorksheet)

View File

@ -596,6 +596,7 @@ type
// Writes out a PANE record
procedure WritePane(AStream: TStream; ASheet: TsWorksheet; IsBiff58: Boolean;
out ActivePane: Byte);
procedure WritePASSWORD(AStream: TStream; const ACryptoInfo: TsCryptoInfo);
// Writes out whether grid lines are printed
procedure WritePrintGridLines(AStream: TStream);
procedure WritePrintHeaders(AStream: TStream);
@ -4038,6 +4039,33 @@ begin
{ Not used (BIFF5-BIFF8 only, not written in BIFF2-BIFF4 }
end;
{@@ ----------------------------------------------------------------------------
Writes a PASSWORD record containing the hash of a password
-------------------------------------------------------------------------------}
procedure TsSpreadBIFFWriter.WritePASSWORD(AStream: TStream;
const ACryptoInfo: TsCryptoInfo);
var
hash: LongInt;
begin
if ACryptoInfo.PasswordHash = '' then
exit;
// Can write only passwords that were hashed using Excel's algorithm.
if ACryptoInfo.Algorithm <> caExcel then begin
Workbook.AddErrorMsg(rsPasswordRemoved_Excel);
exit;
end;
// Excel's hash produces a hex number which is stored by fps as a string.
if not TryStrToInt('$' + ACryptoInfo.PasswordHash, hash) then begin
Workbook.AddErrorMsg(rsPasswordRemoved_NotValid);
exit;
end;
WriteBIFFHeader(AStream, INT_EXCEL_ID_PASSWORD, 2);
AStream.WriteWord(WordToLE(hash));
end;
{@@ ----------------------------------------------------------------------------
Writes out whether grid lines are printed or not
-------------------------------------------------------------------------------}

View File

@ -29,6 +29,7 @@ type
procedure TestWriteRead_WorksheetProtection(AFormat: TsSpreadsheetFormat;
ACondition: Integer);
procedure TestWriteRead_CellProtection(AFormat: TsSpreadsheetFormat);
procedure TestWriteRead_Passwords(AFormat: TsSpreadsheetFormat);
published
// Writes out protection & reads back.
@ -42,6 +43,7 @@ type
procedure TestWriteRead_BIFF2_WorksheetProtection_Objects;
procedure TestWriteRead_BIFF2_CellProtection;
procedure TestWriteRead_BIFF2_Passwords;
{ BIFF5 protection tests }
procedure TestWriteRead_BIFF5_WorkbookProtection_None;
@ -55,6 +57,7 @@ type
procedure TestWriteRead_BIFF5_WorksheetProtection_Objects;
procedure TestWriteRead_BIFF5_CellProtection;
procedure TestWriteRead_BIFF5_Passwords;
{ BIFF8 protection tests }
procedure TestWriteRead_BIFF8_WorkbookProtection_None;
@ -68,6 +71,7 @@ type
procedure TestWriteRead_BIFF8_WorksheetProtection_Objects;
procedure TestWriteRead_BIFF8_CellProtection;
procedure TestWriteRead_BIFF8_Passwords;
{ OOXML protection tests }
procedure TestWriteRead_OOXML_WorkbookProtection_None;
@ -91,6 +95,8 @@ type
procedure TestWriteRead_OOXML_CellProtection;
procedure TestWriteRead_OOXML_Passwords;
{ ODS protection tests }
procedure TestWriteRead_ODS_WorkbookProtection_None;
procedure TestWriteRead_ODS_WorkbookProtection_Struct;
@ -107,6 +113,9 @@ type
implementation
uses
fpsUtils;
const
ProtectionSheet = 'Protection';
@ -339,6 +348,67 @@ begin
end;
end;
procedure TSpreadWriteReadProtectionTests.TestWriteRead_Passwords(
AFormat: TsSpreadsheetFormat);
var
MyWorkbook: TsWorkbook;
MyWorksheet: TsWorksheet;
cell: PCell;
TempFile: string; //write xls/xml to this file and read back from it
bi, si, cinfo: TsCryptoInfo;
msg: String;
begin
TempFile := GetTempFileName;
MyWorkbook := TsWorkbook.Create;
try
MyWorksheet := MyWorkBook.AddWorksheet(ProtectionSheet);
MyWorkbook.Protection := [bpLockStructure];
InitCryptoInfo(bi);
bi.PasswordHash := 'ABCD';
bi.Algorithm := caExcel;
MyWorkbook.CryptoInfo := bi;
MyWorksheet.Protect(true);
if AFormat = sfExcel2 then
si := bi // in BIFF2: use the same crypto info for sheet and book
else begin
InitCryptoInfo(si);
si.PasswordHash := 'DCBA';
si.Algorithm := caExcel;
end;
MyWorksheet.CryptoInfo := si;
MyWorkBook.WriteToFile(TempFile, AFormat, true);
finally
MyWorkbook.Free;
end;
// Open the spreadsheet
MyWorkbook := TsWorkbook.Create;
try
MyWorkbook.ReadFromFile(TempFile, AFormat);
MyWorksheet := MyWorkbook.GetFirstWorksheet;
cInfo := MyWorkbook.CryptoInfo;
CheckEquals(
bi.PasswordHash, cinfo.PasswordHash,
'Workbook protection password hash mismatch'
);
cInfo := MyWorksheet.CryptoInfo;
CheckEquals(
si.PasswordHash, cinfo.PasswordHash,
'Worksheet protection password hash mismatch'
);
finally
MyWorkbook.Free;
DeleteFile(TempFile);
end;
end;
{------------------------------------------------------------------------------}
{ Tests for BIFF2 file format }
@ -380,6 +450,11 @@ begin
TestWriteRead_CellProtection(sfExcel2);
end;
procedure TSpreadWriteReadProtectionTests.TestWriteRead_BIFF2_Passwords;
begin
TestWriteRead_Passwords(sfExcel2);
end;
{------------------------------------------------------------------------------}
{ Tests for BIFF5 file format }
@ -431,6 +506,10 @@ begin
TestWriteRead_CellProtection(sfExcel5);
end;
procedure TSpreadWriteReadProtectionTests.TestWriteRead_BIFF5_Passwords;
begin
TestWriteRead_Passwords(sfExcel5);
end;
{------------------------------------------------------------------------------}
{ Tests for BIFF8 file format }
@ -482,6 +561,10 @@ begin
TestWriteRead_CellProtection(sfExcel8);
end;
procedure TSpreadWriteReadProtectionTests.TestWriteRead_BIFF8_Passwords;
begin
TestWriteRead_Passwords(sfExcel8);
end;
{------------------------------------------------------------------------------}
{ Tests for OOXML file format }
@ -577,6 +660,11 @@ begin
TestWriteRead_CellProtection(sfOOXML);
end;
procedure TSpreadWriteReadProtectionTests.TestWriteRead_OOXML_Passwords;
begin
TestWriteRead_Passwords(sfOpenDocument);
end;
{------------------------------------------------------------------------------}
{ Tests for OpenDocument file format }