From a4457a850e341645eb165af10160b5daf1b5581e Mon Sep 17 00:00:00 2001 From: wp_xxyyzz Date: Tue, 7 Mar 2017 22:02:09 +0000 Subject: [PATCH] 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 --- .../fpspreadsheet/languages/fpsstrings.de.po | 13 +++ .../fpspreadsheet/languages/fpsstrings.fi.po | 12 +++ .../fpspreadsheet/languages/fpsstrings.hu.po | 12 +++ .../fpspreadsheet/languages/fpsstrings.po | 12 +++ .../fpspreadsheet/languages/fpsstrings.ru.po | 12 +++ .../source/common/fpsopendocument.pas | 12 ++- .../source/common/fpsstrings.pas | 5 ++ .../fpspreadsheet/source/common/xlsbiff2.pas | 87 +++++++++++++++++- .../fpspreadsheet/source/common/xlsbiff5.pas | 5 ++ .../fpspreadsheet/source/common/xlsbiff8.pas | 6 +- .../fpspreadsheet/source/common/xlscommon.pas | 28 ++++++ .../fpspreadsheet/tests/protectiontests.pas | 88 +++++++++++++++++++ 12 files changed, 287 insertions(+), 5 deletions(-) diff --git a/components/fpspreadsheet/languages/fpsstrings.de.po b/components/fpspreadsheet/languages/fpsstrings.de.po index 10efa1ba8..fdb0fbcc9 100644 --- a/components/fpspreadsheet/languages/fpsstrings.de.po +++ b/components/fpspreadsheet/languages/fpsstrings.de.po @@ -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" + diff --git a/components/fpspreadsheet/languages/fpsstrings.fi.po b/components/fpspreadsheet/languages/fpsstrings.fi.po index 260beee41..564644063 100644 --- a/components/fpspreadsheet/languages/fpsstrings.fi.po +++ b/components/fpspreadsheet/languages/fpsstrings.fi.po @@ -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 "" diff --git a/components/fpspreadsheet/languages/fpsstrings.hu.po b/components/fpspreadsheet/languages/fpsstrings.hu.po index 4399f4f65..5af914896 100644 --- a/components/fpspreadsheet/languages/fpsstrings.hu.po +++ b/components/fpspreadsheet/languages/fpsstrings.hu.po @@ -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" diff --git a/components/fpspreadsheet/languages/fpsstrings.po b/components/fpspreadsheet/languages/fpsstrings.po index c089e6154..8b5d37908 100644 --- a/components/fpspreadsheet/languages/fpsstrings.po +++ b/components/fpspreadsheet/languages/fpsstrings.po @@ -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 "" diff --git a/components/fpspreadsheet/languages/fpsstrings.ru.po b/components/fpspreadsheet/languages/fpsstrings.ru.po index 72d9fd8a3..010d75c8f 100644 --- a/components/fpspreadsheet/languages/fpsstrings.ru.po +++ b/components/fpspreadsheet/languages/fpsstrings.ru.po @@ -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 "" diff --git a/components/fpspreadsheet/source/common/fpsopendocument.pas b/components/fpspreadsheet/source/common/fpsopendocument.pas index e83f2c52b..cf4bd5bcc 100644 --- a/components/fpspreadsheet/source/common/fpsopendocument.pas +++ b/components/fpspreadsheet/source/common/fpsopendocument.pas @@ -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; diff --git a/components/fpspreadsheet/source/common/fpsstrings.pas b/components/fpspreadsheet/source/common/fpsstrings.pas index b8af0e555..e337ca92c 100644 --- a/components/fpspreadsheet/source/common/fpsstrings.pas +++ b/components/fpspreadsheet/source/common/fpsstrings.pas @@ -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. diff --git a/components/fpspreadsheet/source/common/xlsbiff2.pas b/components/fpspreadsheet/source/common/xlsbiff2.pas index 37665cbe8..215936a23 100644 --- a/components/fpspreadsheet/source/common/xlsbiff2.pas +++ b/components/fpspreadsheet/source/common/xlsbiff2.pas @@ -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 diff --git a/components/fpspreadsheet/source/common/xlsbiff5.pas b/components/fpspreadsheet/source/common/xlsbiff5.pas index d6a101a24..8d0ec3851 100644 --- a/components/fpspreadsheet/source/common/xlsbiff5.pas +++ b/components/fpspreadsheet/source/common/xlsbiff5.pas @@ -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); diff --git a/components/fpspreadsheet/source/common/xlsbiff8.pas b/components/fpspreadsheet/source/common/xlsbiff8.pas index 33d3089a4..0995d136f 100644 --- a/components/fpspreadsheet/source/common/xlsbiff8.pas +++ b/components/fpspreadsheet/source/common/xlsbiff8.pas @@ -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) diff --git a/components/fpspreadsheet/source/common/xlscommon.pas b/components/fpspreadsheet/source/common/xlscommon.pas index fcd42c68c..87f882b31 100644 --- a/components/fpspreadsheet/source/common/xlscommon.pas +++ b/components/fpspreadsheet/source/common/xlscommon.pas @@ -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 -------------------------------------------------------------------------------} diff --git a/components/fpspreadsheet/tests/protectiontests.pas b/components/fpspreadsheet/tests/protectiontests.pas index 84ff146d0..75063beab 100644 --- a/components/fpspreadsheet/tests/protectiontests.pas +++ b/components/fpspreadsheet/tests/protectiontests.pas @@ -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 }