fpspreadsheet: Improved password handling and format detection for the decryption readers.

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@8913 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2023-08-05 22:20:06 +00:00
parent e1291123ef
commit 5464272445
8 changed files with 200 additions and 51 deletions

View File

@ -156,7 +156,7 @@ type
var AFontColor: TsColor);
function ReadHeaderFooterText(ANode: TDOMNode): String;
procedure ReadMetaData(ANode: TDOMNode);
procedure ReadMetaInfManifest(ANode: TDOMNode; out IsEncrypted: Boolean);
procedure ReadMetaInfManifest(ANode: TDOMNode);
procedure ReadPictures(AStream: TStream);
procedure ReadPrintRanges(ATableNode: TDOMNode; ASheet: TsBasicWorksheet);
procedure ReadRowsAndCells(ATableNode: TDOMNode);
@ -177,6 +177,7 @@ type
protected
FPointSeparatorSettings: TFormatSettings;
procedure AddBuiltinNumFormats; override;
function NeedsPassword(AStream: TStream): Boolean; override;
procedure ReadAutomaticStyles(AStylesNode: TDOMNode);
procedure ReadMasterStyles(AStylesNode: TDOMNode);
procedure ReadNumFormats(AStylesNode: TDOMNode);
@ -203,7 +204,6 @@ type
function Decrypt(AStream: TStream; ADecryptionInfo: TsOpenDocManifestFileEntry;
APassword: String; ADestStream: TStream; out AErrorMsg: String): Boolean; virtual;
function SupportsDecryption: Boolean; virtual;
function UnzipToStream(AStream: TStream; AZippedFile: String;
ADestStream: TStream): Boolean; virtual;
function UnzipToStream(AStream: TStream; AZippedFile: String;
@ -1630,6 +1630,22 @@ begin
Result := -1;
end;
{ Returns true if the file is password-protected, i.e. if there are entries
in the META-INF/manifest.xml while contain decryption information. }
function TsSpreadOpenDocReader.NeedsPassword(AStream: TStream): Boolean;
var
i: Integer;
begin
Unused(AStream);
for i := 0 to FManifestFileEntries.Count-1 do
if TsOpenDocManifestFileEntry(FManifestFileEntries[i]).Encrypted then
begin
Result := true;
exit;
end;
Result := false;
end;
function TsSpreadOpenDocReader.NodeIsEmptyCell(ACellNode: TDOMNode): Boolean;
var
valuestr: String;
@ -2062,8 +2078,7 @@ end;
// Reads the file META-INF/manifest.xml. It is never encrypted and contains
// decryption information.
procedure TsSpreadOpenDocReader.ReadMetaInfManifest(ANode: TDOMNode;
out IsEncrypted: Boolean);
procedure TsSpreadOpenDocReader.ReadMetaInfManifest(ANode: TDOMNode);
function GetAlgorithmName(ASubNode: TDOMNode; AttrName: String): String;
var
@ -2093,7 +2108,6 @@ var
nodeName: String;
entry: TsOpenDocManifestFileEntry;
begin
IsEncrypted := false;
while ANode <> nil do
begin
nodeName := ANode.NodeName;
@ -2107,7 +2121,6 @@ begin
nodeName := encryptionDataNode.NodeName;
if nodeName = 'manifest:encryption-data' then
begin
IsEncrypted := true;
entry.Encrypted := true;
entry.EncryptionData_ChecksumType := GetAlgorithmName(encryptionDataNode, 'manifest:checksum-type');
entry.EncryptionData_Checksum := GetAttrValue(encryptionDataNode, 'manifest:checksum');
@ -2924,9 +2937,8 @@ begin
ReadXMLStream(Doc, XMLStream);
if Assigned(Doc) then
begin
ReadMetaInfManifest(Doc.DocumentElement.FindNode('manifest:file-entry'), isEncrypted);
if isEncrypted and not SupportsDecryption then
raise EFpSpreadsheetReader.Create('File is encrypted.');
ReadMetaInfManifest(Doc.DocumentElement.FindNode('manifest:file-entry'));
CheckPassword(XMLStream, APassword);
end;
end;
finally
@ -5612,13 +5624,6 @@ begin
Result := false;
end;
{ If a descendant reader class supports decryption it must return true
here. The standard ods reader is not able to read encrypted file content. }
function TsSpreadOpenDocReader.SupportsDecryption: Boolean;
begin
Result := false;
end;
function TsSpreadOpenDocReader.UnzipToStream(AStream: TStream;
AZippedFile: String; ADestStream: TStream): Boolean;
begin

View File

@ -48,12 +48,16 @@ type
public
{ File format detection }
class function CheckFileFormat(AStream: TStream): boolean; virtual; abstract;
{ General writing methods }
{ General reading methods }
procedure ReadFromFile(AFileName: string; APassword: String = '';
AParams: TsStreamParams = []); virtual; abstract;
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); virtual; abstract;
procedure ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); virtual; abstract;
{ Related to password-protected files }
procedure CheckPassword(AStream: TStream; var APassword: String); virtual;
function NeedsPassword(AStream: TStream): Boolean; virtual;
function SupportsDecryption: Boolean; virtual;
end;
{ TsBasicSpreadWriter }
@ -263,6 +267,53 @@ begin
end;
{------------------------------------------------------------------------------}
{ TsBasicSpreadReader }
{------------------------------------------------------------------------------}
{@@ ----------------------------------------------------------------------------
Checks whether the currently processed stream is password-protected.
If true, it checks whether the reader class supports decryption and makes
sure that a password is provided to the calling routine (ReadFromStream).
Exceptions are raised in the error cases.
Must be called at the beginning of ReadFromStream when the stream potentially
can be decrypted in order to provide the user a reasonable error message.
-------------------------------------------------------------------------------}
procedure TsBasicSpreadReader.CheckPassword(AStream: TStream;
var APassword: String);
begin
if NeedsPassword(AStream) then
begin
if SupportsDecryption then
begin
if (APassword = '') and Assigned(FWorkbook.OnQueryPassword) then
APassword := FWorkbook.OnQueryPassword();
if (APassword = '') then
raise EFpSpreadsheetReader.Create('Password required to open this workbook.');
end else
raise EFpSpreadsheetReader.Create('File is encrypted.');
end;
end;
{@@ ----------------------------------------------------------------------------
Should return whether the workbook to be loaded is password-protected by
encryption.
-------------------------------------------------------------------------------}
function TsBasicSpreadReader.NeedsPassword(AStream: TStream): Boolean;
begin
Unused(AStream);
Result := false;
end;
{@@ ----------------------------------------------------------------------------
Returns true if this reader class is able to decrypt a password-protected
workbook file/stream.
-------------------------------------------------------------------------------}
function TsBasicSpreadReader.SupportsDecryption: Boolean;
begin
Result := false;
end;
{------------------------------------------------------------------------------}
{ TsBasicSpreadWriter }
{------------------------------------------------------------------------------}
@ -507,7 +558,9 @@ end;
Opens the file and calls ReadFromStream. Data are stored in the workbook
specified during construction.
@param AFileName The input file name.
@param (AFileName The input file name.)
@param (APassword The password needed to open a password-protected workbook.
Note: Password-protected files are not supported by all readers.)
@see TsWorkbook
-------------------------------------------------------------------------------}
procedure TsCustomSpreadReader.ReadFromFile(AFileName: string;
@ -545,11 +598,9 @@ end;
Its basic implementation here assumes that the stream is a TStringStream and
the data are provided by calling ReadFromStrings. This mechanism is valid
for wikitables.
for wiki-tables.
Data will be stored in the workbook defined at construction.
@param AData Workbook which is filled by the data from the stream.
-------------------------------------------------------------------------------}
procedure TsCustomSpreadReader.ReadFromStream(AStream: TStream;
APassword: String; AParams: TsStreamParams = []);
@ -573,7 +624,7 @@ end;
{@@ ----------------------------------------------------------------------------
Reads workbook data from a string list. This abstract implementation does
nothing and raises an exception. Must be overridden, like for wikitables.
nothing and raises an exception. Must be overridden, like for wiki-tables.
-------------------------------------------------------------------------------}
procedure TsCustomSpreadReader.ReadFromStrings(AStrings: TStrings;
AParams: TsStreamParams = []);
@ -593,8 +644,8 @@ end;
Creates an internal instance of the number format list according to the
file format being read/written.
@param AWorkbook Workbook from with the file is written. This parameter is
passed from the workbook which creates the writer.
@param (AWorkbook Workbook from which the file is written. This parameter is
passed from the workbook which creates the writer.)
-------------------------------------------------------------------------------}
constructor TsCustomSpreadWriter.Create(AWorkbook: TsBasicWorkbook);
begin

View File

@ -1228,6 +1228,8 @@ type
property Keywords: TStrings read FKeywords write FKeywords;
end;
TsOnQueryPassword = function: String of object;
{@@ Basic worksheet class to avoid circular unit references. It has only those
properties and methods which do not require any other unit than fpstypes. }
TsBasicWorksheet = class
@ -1256,6 +1258,7 @@ type
TsBasicWorkbook = class
private
FLog: TStringList;
FOnQueryPassword: TsOnQueryPassword;
function GetErrorMsg: String;
protected
FFileName: String;
@ -1292,6 +1295,8 @@ type
property Protection: TsWorkbookProtections read FProtection write FProtection;
{@@ Units of row heights and column widths }
property Units: TsSizeUnits read FUnits;
{@@ Event returning the password to open a password-protected workbook }
property OnQueryPassword: TsOnQueryPassword read FOnQueryPassword write FOnQueryPassword;
end;
{@@ Ancestor of the fpSpreadsheet exceptions }

View File

@ -141,6 +141,8 @@ type
protected
FFirstNumFormatIndexInFile: Integer;
procedure AddBuiltinNumFormats; override;
class function IsEncrypted(AStream: TStream): Boolean;
function NeedsPassword(AStream: TStream): Boolean; override;
public
constructor Create(AWorkbook: TsBasicWorkbook); override;
destructor Destroy; override;
@ -359,6 +361,8 @@ const
LAST_PALETTE_INDEX = 63;
CFB_SIGNATURE = $E11AB1A1E011CFD0; // Compound File Binary Signature
type
TFillListData = class
PatternType: String;
@ -951,6 +955,25 @@ begin
Result := TMemoryStream.Create;
end;
{ Checks the file header for the signature of the decrypted file format. }
class function TsSpreadOOXMLReader.IsEncrypted(AStream: TStream): Boolean;
var
p: Int64;
buf: Cardinal;
begin
p := AStream.Position;
AStream.Position := 0;
AStream.Read(buf, SizeOf(buf));
Result := (buf = Cardinal(CFB_SIGNATURE));
AStream.Position := p;
end;
{ Returns TRUE if the file is encrypted and requires a password. }
function TsSpreadOOXMLReader.NeedsPassword(AStream: TStream): Boolean;
begin
Result := IsEncrypted(AStream);
end;
procedure TsSpreadOOXMLReader.ReadActiveSheet(ANode: TDOMNode;
out ActiveSheetIndex: Integer);
var
@ -4411,6 +4434,8 @@ begin
Unused(APassword, AParams);
Doc := nil;
CheckPassword(AStream, APassword);
try
// Retrieve theme colors
XMLStream := CreateXMLStream;

View File

@ -10,7 +10,7 @@ uses
{$IFDEF UNZIP_ABBREVIA}
ABUnzper,
{$ENDIF}
fpsTypes, fpsOpenDocument;
fpsTypes, fpsUtils, fpsOpenDocument;
type
TsSpreadOpenDocReaderCrypto = class(TsSpreadOpenDocReader)
@ -141,7 +141,6 @@ begin
raise EFpSpreadsheetReader.Create('Unsupported key generation method ' + ADecryptionInfo.KeyDerivationName);
end;
{ Tells the calling routine that this reader is able to decrypt ods files. }
function TsSpreadOpenDocReaderCrypto.SupportsDecryption: Boolean;
begin

View File

@ -18,7 +18,7 @@ uses
CFB_Signature = $E11AB1A1E011CFD0; // Compound File Binary Signature
// Weird is the documentation is equal to
// $D0CF11E0A1B11AE1, but here is inversed
// maybe related to litle endian thing?!!
// maybe related to little endian thing?!!
// EncryptionHeaderFlags as defined in 2.3.1 [MS-OFFCRYPTO]
ehfAES = $00000004;

View File

@ -6,11 +6,17 @@ interface
uses
Classes,
fpstypes, xlsxooxml, xlsxdecrypter;
fpstypes, fpsUtils, xlsxooxml, xlsxdecrypter;
type
TsSpreadOOXMLReaderCrypto = class(TsSpreadOOXMLReader)
private
FNeedsPassword: Boolean;
protected
function NeedsPassword(AStream: TStream): Boolean; override;
function SupportsDecryption: Boolean; override;
public
class function CheckFileFormat(AStream: TStream): boolean; override;
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); override;
end;
@ -24,17 +30,34 @@ implementation
uses
fpsReaderWriter;
class function TsSpreadOOXMLReaderCrypto.CheckFileFormat(AStream: TStream): boolean;
begin
Result := inherited; // This checks for a normal xlsx format ...
if not Result then
Result := IsEncrypted(AStream); // ... and this for a decrypted one.
end;
function TsSpreadOOXMLReaderCrypto.NeedsPassword(AStream: TStream): Boolean;
begin
Unused(AStream);
Result := FNeedsPassword;
end;
procedure TsSpreadOOXMLReaderCrypto.ReadFromStream(AStream: TStream;
APassword: String = ''; AParams: TsStreamParams = []);
var
ExcelDecrypt : TExcelFileDecryptor;
DecryptedStream: TStream;
begin
FNeedsPassword := false;
ExcelDecrypt := TExcelFileDecryptor.Create;
try
AStream.Position := 0;
if ExcelDecrypt.isEncryptedAndSupported(AStream) then
begin
FNeedsPassword := true;
CheckPassword(AStream, APassword);
DecryptedStream := TMemoryStream.Create;
try
ExcelDecrypt.Decrypt(AStream, DecryptedStream, UnicodeString(APassword));
@ -42,12 +65,9 @@ begin
AStream.Free;
AStream := TMemoryStream.Create;
DecryptedStream.Position := 0;
TMemoryStream(decryptedStream).SaveToFile('decr.zip');
DecryptedStream.Position := 0;
AStream.CopyFrom(DecryptedStream, DecryptedStream.Size);
AStream.Position := 0;
FNeedsPassword := false; // AStream is not encrypted any more.
finally
DecryptedStream.Free;
end;
@ -60,6 +80,11 @@ begin
inherited;
end;
function TsSpreadOOXMLReaderCrypto.SupportsDecryption: Boolean;
begin
Result := true;
end;
initialization

View File

@ -27,8 +27,8 @@ unit fpspreadsheetctrls;
interface
uses
LMessages, LResources, LCLVersion,
Classes, Graphics, SysUtils, Controls, StdCtrls, ComCtrls, ValEdit, ActnList,
LCLType, LCLIntf, LCLProc, LCLVersion, LMessages, LResources,
Classes, Types, Graphics, SysUtils, Controls, StdCtrls, ComCtrls, ValEdit, ActnList,
fpstypes, fpspreadsheet;
const
@ -70,6 +70,7 @@ type
FPendingOperation: TsCopyOperation;
FOptions: TsWorkbookOptions;
FOnError: TsWorkbookSourceErrorEvent;
FOnQueryPassword: TsOnQueryPassword;
// Getters / setters
function GetFileFormat: TsSpreadsheetFormat;
@ -96,10 +97,11 @@ type
protected
procedure AbortSelection;
function DoQueryPassword: String;
procedure DoShowError(const AErrorMsg: String);
procedure InternalCreateNewWorkbook(AWorkbook: TsWorkbook = nil);
procedure InternalLoadFromFile(AFileName: string; AAutoDetect: Boolean;
AFormatID: TsSpreadFormatID; AWorksheetIndex: Integer = -1);
AFormatID: TsSpreadFormatID; AWorksheetIndex: Integer; APassword: String);
procedure InternalLoadFromWorkbook(AWorkbook: TsWorkbook;
AWorksheetIndex: Integer = -1);
procedure Loaded; override;
@ -116,15 +118,13 @@ type
public
procedure CreateNewWorkbook;
procedure LoadFromProtectedSpreadsheetFile(AFileName: String;
AFormatID: TsSpreadFormatID; APassword: String; AWorksheetIndex: Integer = -1);
procedure LoadFromSpreadsheetFile(AFileName: string;
AFormat: TsSpreadsheetFormat; AWorksheetIndex: Integer = -1); overload;
procedure LoadFromSpreadsheetFile(AFileName: string;
AFormatID: TsSpreadFormatID = sfidUnknown; AWorksheetIndex: Integer = -1); overload;
procedure LoadFromWorkbook(AWorkbook: TsWorkbook; AWorksheetIndex: Integer = -1);
{
procedure LoadFromSpreadsheetFile(AFileName: string;
AWorksheetIndex: Integer = -1); overload;
}
procedure SaveToSpreadsheetFile(AFileName: string;
AOverwriteExisting: Boolean = true); overload;
@ -133,9 +133,6 @@ type
procedure SaveToSpreadsheetFile(AFileName: string; AFormatID: TsSpreadFormatID;
AOverwriteExisting: Boolean = true); overload;
// procedure DisableControls;
// procedure EnableControls;
procedure SelectCell(ASheetRow, ASheetCol: Cardinal);
procedure SelectWorksheet(AWorkSheet: TsWorksheet);
@ -183,6 +180,8 @@ type
{@@ A message box is displayey if an error occurs during loading of a
spreadsheet. This behavior can be replaced by means of the event OnError. }
property OnError: TsWorkbookSourceErrorEvent read FOnError write FOnError;
{@@ Event fired when a password is required. Handler must return the pwd. }
property OnQueryPassword: TsOnQueryPassword read FOnQueryPassword write FOnQueryPassword;
end;
@ -710,8 +709,7 @@ function ScalePPI(ALength: Integer): Integer;
implementation
uses
Types, Math, StrUtils, TypInfo, LCLType, LCLIntf, LCLProc,
Dialogs, Forms, Clipbrd,
Math, StrUtils, TypInfo, Dialogs, Forms, Clipbrd,
fpsStrings, fpsCrypto, fpsReaderWriter, fpsUtils, fpsNumFormat, fpsImages,
fpsHTMLUtils, fpsExprParser;
@ -1016,6 +1014,22 @@ begin
SelectWorksheet(FWorksheet);
end;
function TsWorkbookSource.DoQueryPassword: String;
var
crs: TCursor;
begin
crs := Screen.Cursor;
Screen.Cursor := crDefault;
try
if Assigned(FOnQueryPassword) then
Result := FOnQueryPassword()
else
Result := InputBox('Password required to open workbook', 'Password', '');
finally
Screen.Cursor := crs;
end;
end;
{@@ ----------------------------------------------------------------------------
An error has occured during loading of the workbook. Shows a message box by
default. But a different behavior can be obtained by means of the OnError
@ -1137,20 +1151,23 @@ end;
for the loader.
Is ignored when AAutoDetect is @false.)
@param(AWorksheetIndex Index of the worksheet to be selected after loading.)
@param(APassword Password to open encrypted workbook. Note: this is
supported only by ods and xlsx readers.)
-------------------------------------------------------------------------------}
procedure TsWorkbookSource.InternalLoadFromFile(AFileName: string;
AAutoDetect: Boolean; AFormatID: TsSpreadFormatID;
AWorksheetIndex: Integer = -1);
AAutoDetect: Boolean; AFormatID: TsSpreadFormatID; AWorksheetIndex: Integer;
APassword: String);
var
book: TsWorkbook;
begin
book := TsWorkbook.Create;
try
book.Options := FOptions;
book.OnQueryPassword := @DoQueryPassword;
if AAutoDetect then
book.ReadfromFile(AFileName)
book.ReadfromFile(AFileName, APassword)
else
book.ReadFromFile(AFileName, AFormatID);
book.ReadFromFile(AFileName, AFormatID, APassword);
InternalLoadFromWorkbook(book, AWorksheetIndex);
except
// book is normally used as current workbook. But it must be destroyed
@ -1220,9 +1237,11 @@ begin
end;
{@@ ----------------------------------------------------------------------------
Public spreadsheet loader to be used if file format is known.
Public loader of a spreadsheet file.
Call this methdd for both built-in and user-provided file formats.
Call this method for both built-in and user-provided file formats.
If the workbook is password-protected the password is prompted by a dialog.
@param(AFilename Name of the spreadsheet file to be loaded.)
@param(AFormatID Identifier of the spreadsheet file format assumed
@ -1236,7 +1255,7 @@ var
autodetect: Boolean;
begin
autodetect := (AFormatID = sfidUnknown);
InternalLoadFromFile(AFileName, autodetect, AFormatID, AWorksheetIndex);
InternalLoadFromFile(AFileName, autodetect, AFormatID, AWorksheetIndex, '');
end;
(*
{@@ ------------------------------------------------------------------------------
@ -1272,6 +1291,26 @@ begin
InternalLoadFromWorkbook(AWorkbook, AWorksheetIndex);
end;
{@@ ----------------------------------------------------------------------------
Public loader of a spreadsheet file.
Should be called in case of password-protected files when the password is
already known and no password dialog should appear.
Call this method for both built-in and user-provided file formats.
@param(AFilename Name of the spreadsheet file to be loaded.)
@param(AFormatID Identifier of the spreadsheet file format assumed
for the file.)
@param(AWorksheetIndex Index of the worksheet to be selected after loading.
(If empty then the active worksheet is loaded) )
-------------------------------------------------------------------------------}
procedure TsWorkbookSource.LoadFromProtectedSpreadsheetFile(AFileName: String;
AFormatID: TsSpreadFormatID; APassword: String; AWorksheetIndex: Integer = -1);
begin
InternalLoadFromFile(AFileName, false, AFormatID, AWorksheetIndex, APassword);
end;
{@@ ----------------------------------------------------------------------------
Notifies listeners of workbook, worksheet, cell, or selection changes.
The changed item is identified by the parameter AChangedItems.