fpspreadsheet: Initial commit of experimental xlsx decryption support (xlsx decryptor written by forum user shobits1).

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@5806 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2017-03-14 21:48:29 +00:00
parent 29f8b2a131
commit d25851c07f
17 changed files with 934 additions and 48 deletions

View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<CONFIG>
<ProjectOptions>
<Version Value="10"/>
<PathDelim Value="\"/>
<General>
<Flags>
<LRSInOutputDirectory Value="False"/>
</Flags>
<SessionStorage Value="InProjectDir"/>
<MainUnit Value="0"/>
<Title Value="ooxml_decrypt_and_read"/>
<UseAppBundle Value="False"/>
</General>
<BuildModes Count="1">
<Item1 Name="Default" Default="True"/>
</BuildModes>
<PublishOptions>
<Version Value="2"/>
<IgnoreBinaries Value="False"/>
<IncludeFileFilter Value="*.(pas|pp|inc|lfm|lpr|lrs|lpi|lpk|sh|xml)"/>
<ExcludeFileFilter Value="*.(bak|ppu|ppw|o|so);*~;backup"/>
</PublishOptions>
<RunParams>
<local>
<FormatVersion Value="1"/>
<LaunchingApplication PathPlusParams="\usr\X11R6\bin\xterm -T 'Lazarus Run Output' -e $(LazarusDir)\tools\runwait.sh $(TargetCmdLine)"/>
</local>
</RunParams>
<RequiredPackages Count="3">
<Item1>
<PackageName Value="laz_fpspreadsheet_crypto"/>
</Item1>
<Item2>
<PackageName Value="laz_fpspreadsheet"/>
</Item2>
<Item3>
<PackageName Value="LazUtils"/>
</Item3>
</RequiredPackages>
<Units Count="1">
<Unit0>
<Filename Value="ooxml_decrypt_and_read.pas"/>
<IsPartOfProject Value="True"/>
</Unit0>
</Units>
</ProjectOptions>
<CompilerOptions>
<Version Value="11"/>
<PathDelim Value="\"/>
<Target>
<Filename Value="ooxml_decrypt_and_read"/>
</Target>
<SearchPaths>
<OtherUnitFiles Value="..\..\..\source\common"/>
<UnitOutputDirectory Value="..\..\lib\$(TargetCPU)-$(TargetOS)"/>
</SearchPaths>
<Parsing>
<SyntaxOptions>
<UseAnsiStrings Value="False"/>
</SyntaxOptions>
</Parsing>
<Linking>
<Debugging>
<DebugInfoType Value="dsDwarf2Set"/>
<UseExternalDbgSyms Value="True"/>
</Debugging>
</Linking>
</CompilerOptions>
</CONFIG>

View File

@ -0,0 +1,84 @@
{
ooxml_decrypt_and_read.lpr
Demonstrates how to read an Excel 2007 xlsx file which is workbook-protected
and thus encrypted by an internal password.
Basic operating procedure
- Add package laz_fpspreadsheet_crypto
- Use xlsxooxml_crypto (instead of xlsxooxml)
- In Workbook.ReadFromFile specify the file format id spfidOOXML_crypto instead
of the the file format sfOOXML.
}
program ooxml_decrypt_and_read;
{$mode delphi}{$H+}
uses
Classes, SysUtils, LazUTF8, fpstypes, fpspreadsheet, laz_fpspreadsheet,
xlsxooxml_crypto;
var
MyWorkbook: TsWorkbook;
MyWorksheet: TsWorksheet;
InputFilename: String;
MyDir: string;
cell: PCell;
i: Integer;
password: String;
Prot_enc: Integer = 1; // 0 - protected, 1 - encrypted workbook
begin
MyDir := ExtractFilePath(ParamStr(0));
// Open the input file
MyDir := ExtractFilePath(ParamStr(0));
case Prot_enc of
0: begin
InputFileName := MyDir + 'protected_workbook.xlsx';
password := '';
end;
1: begin
InputFileName := MyDir + 'encrypted_workbook.xlsx';
password := 'test';
end;
end;
if not FileExists(InputFileName) then begin
WriteLn('Input file ', InputFileName, ' does not exist. Please run opendocwrite first.');
Halt;
end;
WriteLn('Opening input file ', InputFilename);
// Create the spreadsheet
MyWorkbook := TsWorkbook.Create;
MyWorkbook.Options := MyWorkbook.Options + [boReadFormulas];
MyWorkbook.ReadFromFile(InputFilename, sfidOOXML_crypto, password);
MyWorksheet := MyWorkbook.GetFirstWorksheet;
// Write all cells with contents to the console
WriteLn('');
WriteLn('Contents of the first worksheet of the file:');
WriteLn('');
for cell in MyWorksheet.Cells do
WriteLn(
'Row: ', cell^.Row,
' Col: ', cell^.Col,
' Value: ', UTF8ToConsole(MyWorkSheet.ReadAsText(cell^.Row, cell^.Col))
);
// Finalization
MyWorkbook.Free;
{$ifdef WINDOWS}
WriteLn;
WriteLn('Press ENTER to quit...');
ReadLn;
{$ENDIF}
end.

View File

@ -0,0 +1,8 @@
This demo program reads encrypted xlsx files.
File "encrypted_workbook.xlsx" was encrypted in the File menu of Excel 2007.
The password is "test".
The file "protected_workbook.xlsx" was worksheet-protected in the Inspect
menu of Excel 2007. The password "test" was specified, it is not the password
used for encryption of the file, it is only needed to unlock worksheet protection.

View File

@ -22,8 +22,10 @@ type
procedure ReadNumber(AStream: TStream); override;
public
constructor Create(AWorkbook: TsWorkbook); override;
procedure ReadFromFile(AFileName: String; AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); override;
procedure ReadFromFile(AFileName: String; APassword: String = '';
AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); override;
procedure ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); override;
end;
@ -173,14 +175,14 @@ begin
Unused(AStream);
end;
procedure TsCSVReader.ReadFromFile(AFileName: String;
procedure TsCSVReader.ReadFromFile(AFileName: String; APassword: String = '';
AParams: TsStreamParams = []);
begin
FWorksheetName := ChangeFileExt(ExtractFileName(AFileName), '');
inherited ReadFromFile(AFilename, AParams);
inherited ReadFromFile(AFilename, APassword, AParams);
end;
procedure TsCSVReader.ReadFromStream(AStream: TStream;
procedure TsCSVReader.ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []);
var
Parser: TCSVParser;
@ -227,7 +229,7 @@ var
begin
Stream := TStringStream.Create(AStrings.Text);
try
ReadFromStream(Stream, AParams);
ReadFromStream(Stream, '', AParams);
finally
Stream.Free;
end;

View File

@ -57,7 +57,8 @@ type
public
constructor Create(AWorkbook: TsWorkbook); override;
destructor Destroy; override;
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); override;
procedure ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); override;
end;
@ -1045,11 +1046,12 @@ begin
SetLength(FCurrRichTextParams, 0);
end;
procedure TsHTMLReader.ReadFromStream(AStream: TStream;
procedure TsHTMLReader.ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []);
var
list: TStringList;
begin
Unused(APassword);
list := TStringList.Create;
try
list.LoadFromStream(AStream);

View File

@ -165,7 +165,8 @@ type
destructor Destroy; override;
{ General reading methods }
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream;
APassword: String = ''; AParams: TsStreamParams = []); override;
end;
{ TsSpreadOpenDocWriter }
@ -2422,7 +2423,7 @@ begin
end;
procedure TsSpreadOpenDocReader.ReadFromStream(AStream: TStream;
AParams: TsStreamParams = []);
APassword: String = ''; AParams: TsStreamParams = []);
var
Doc : TXMLDocument;
BodyNode, SpreadSheetNode, TableNode: TDOMNode;
@ -2445,7 +2446,7 @@ var
end;
begin
Unused(AParams);
Unused(APassword, AParams);
Doc := nil;
try

View File

@ -756,15 +756,15 @@ type
destructor Destroy; override;
procedure ReadFromFile(AFileName: string; AFormatID: TsSpreadFormatID;
AParams: TsStreamParams = []); overload;
APassword: String = ''; AParams: TsStreamParams = []); overload;
procedure ReadFromFile(AFileName: string; AFormat: TsSpreadsheetFormat;
AParams: TsStreamParams = []); overload;
procedure ReadFromFile(AFileName: string;
procedure ReadFromFile(AFileName: string; APassword: String = '';
AParams: TsStreamParams = []); overload;
procedure ReadFromFileIgnoringExtension(AFileName: string;
AParams: TsStreamParams = []);
APassword: String = ''; AParams: TsStreamParams = []);
procedure ReadFromStream(AStream: TStream; AFormatID: TsSpreadFormatID;
AParams: TsStreamParams = []); overload;
APassword: String = ''; AParams: TsStreamParams = []); overload;
procedure ReadFromStream(AStream: TStream; AFormat: TsSpreadsheetFormat;
AParams: TsStreamParams = []); overload;
@ -8199,7 +8199,7 @@ procedure TsWorkbook.ReadFromFile(AFileName: string;
begin
if AFormat = sfUser then
raise Exception.Create('[TsWorkbook.ReadFromFile] Don''t call this method for user-provided file formats.');
ReadFromFile(AFilename, ord(AFormat), AParams);
ReadFromFile(AFilename, ord(AFormat), '', AParams);
end;
{@@ ----------------------------------------------------------------------------
@ -8209,8 +8209,8 @@ end;
@param AFileName Name of the file to be read
@param AFormatID Identifier of the file format assumed
-------------------------------------------------------------------------------}
procedure TsWorkbook.ReadFromFile(AFileName: string;
AFormatID: TsSpreadFormatID; AParams: TsStreamParams = []);
procedure TsWorkbook.ReadFromFile(AFileName: string; AFormatID: TsSpreadFormatID;
APassword: String = ''; AParams: TsStreamParams = []);
var
AReader: TsBasicSpreadReader;
ok: Boolean;
@ -8219,7 +8219,7 @@ begin
raise Exception.CreateFmt(rsFileNotFound, [AFileName]);
if AFormatID = sfIDUnknown then begin
ReadFromFile(AFileName, AParams);
ReadFromFile(AFileName, APassword, AParams);
exit;
end;
@ -8231,7 +8231,7 @@ begin
FReadWriteFlag := rwfRead;
inc(FLockCount); // This locks various notifications from being sent
try
AReader.ReadFromFile(AFileName, AParams);
AReader.ReadFromFile(AFileName, APassword, AParams);
ok := true;
UpdateCaches;
if (boAutoCalc in Options) then
@ -8253,7 +8253,8 @@ end;
the extension. In the case of the ambiguous xls extension, it will simply
assume that it is BIFF8. Note that it could be BIFF2 or 5 as well.
-------------------------------------------------------------------------------}
procedure TsWorkbook.ReadFromFile(AFileName: string; AParams: TsStreamParams = []);
procedure TsWorkbook.ReadFromFile(AFileName: string; APassword: String = '';
AParams: TsStreamParams = []);
var
formatID: TsSpreadFormatID;
canLoad, success: Boolean;
@ -8293,7 +8294,7 @@ begin
success := false;
for i:=0 to High(fileformats) do begin
try
ReadFromFile(AFileName, fileformats[i], AParams);
ReadFromFile(AFileName, fileformats[i], APassword, AParams);
success := true;
break; // Exit the loop if we reach this point successfully.
except
@ -8310,7 +8311,7 @@ end;
Reads the document from a file, but ignores the extension.
-------------------------------------------------------------------------------}
procedure TsWorkbook.ReadFromFileIgnoringExtension(AFileName: string;
AParams: TsStreamParams = []);
APassword: String = ''; AParams: TsStreamParams = []);
var
formatID: TsSpreadFormatID;
fileformats: TsSpreadFormatIDArray;
@ -8319,7 +8320,7 @@ begin
fileformats := GetSpreadFormats(faRead, [ord(sfOOXML), ord(sfOpenDocument), ord(sfExcel8)]);
for formatID in fileformats do begin
try
ReadFromFile(AFileName, formatID, AParams);
ReadFromFile(AFileName, formatID, APassword, AParams);
success := true;
break;
except
@ -8342,7 +8343,7 @@ procedure TsWorkbook.ReadFromStream(AStream: TStream;
begin
if AFormat = sfUser then
raise Exception.Create('[TsWorkbook.ReadFromFile] Don''t call this method for user-provided file formats.');
ReadFromStream(AStream, ord(AFormat), AParams);
ReadFromStream(AStream, ord(AFormat), '', AParams);
end;
{@@ ----------------------------------------------------------------------------
@ -8352,8 +8353,8 @@ end;
@param AFormatID Identifier of the file format assumed.
@param AParams Optional parameters to control stream access.
-------------------------------------------------------------------------------}
procedure TsWorkbook.ReadFromStream(AStream: TStream;
AFormatID: TsSpreadFormatID; AParams: TsStreamParams = []);
procedure TsWorkbook.ReadFromStream(AStream: TStream; AFormatID: TsSpreadFormatID;
APassword: String = ''; AParams: TsStreamParams = []);
var
AReader: TsBasicSpreadReader;
ok: Boolean;
@ -8366,7 +8367,7 @@ begin
inc(FLockCount);
try
AStream.Position := 0;
AReader.ReadFromStream(AStream, AParams);
AReader.ReadFromStream(AStream, APassword, AParams);
ok := true;
UpdateCaches;
if (boAutoCalc in Options) then

View File

@ -47,8 +47,10 @@ type
TsBasicSpreadReader = class(TsBasicSpreadReaderWriter)
public
{ General writing methods }
procedure ReadFromFile(AFileName: string; AParams: TsStreamParams = []); virtual; abstract;
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); virtual; abstract;
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;
end;
@ -112,8 +114,10 @@ type
destructor Destroy; override;
{ General writing methods }
procedure ReadFromFile(AFileName: string; AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); override;
procedure ReadFromFile(AFileName: string; APassword: String = '';
AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); override;
procedure ReadFromStrings(AStrings: TStrings; AParams: TsStreamParams = []); override;
{@@ List of number formats found in the workbook. }
@ -460,7 +464,7 @@ end;
@see TsWorkbook
-------------------------------------------------------------------------------}
procedure TsCustomSpreadReader.ReadFromFile(AFileName: string;
AParams: TsStreamParams = []);
APassword: String = ''; AParams: TsStreamParams = []);
var
stream, fs: TStream;
begin
@ -482,7 +486,7 @@ begin
end;
try
ReadFromStream(stream, AParams);
ReadFromStream(stream, APassword, AParams);
finally
stream.Free;
end;
@ -501,11 +505,12 @@ end;
@param AData Workbook which is filled by the data from the stream.
-------------------------------------------------------------------------------}
procedure TsCustomSpreadReader.ReadFromStream(AStream: TStream;
AParams: TsStreamParams = []);
APassword: String; AParams: TsStreamParams = []);
var
AStringStream: TStringStream;
AStrings: TStringList;
begin
Unused(APassword);
AStringStream := TStringStream.Create('');
AStrings := TStringList.Create;
try

View File

@ -77,7 +77,8 @@ type
public
constructor Create(AWorkbook: TsWorkbook); override;
{ General reading methods }
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); override;
end;
@ -576,14 +577,14 @@ end;
procedure TsSpreadBIFF2Reader.ReadFromStream(AStream: TStream;
AParams: TsStreamParams = []);
APassword: String = ''; AParams: TsStreamParams = []);
var
BIFF2EOF: Boolean;
RecordType: Word;
CurStreamPos: Int64;
BOFFound: Boolean;
begin
Unused(AParams);
Unused(APassword, AParams);
BIFF2EOF := False;
{ In BIFF2 files there is only one worksheet, let's create it }

View File

@ -89,7 +89,8 @@ type
procedure ReadXF(AStream: TStream);
public
{ General reading methods }
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); override;
end;
{ TsSpreadBIFF5Writer }
@ -925,13 +926,13 @@ begin
end;
procedure TsSpreadBIFF5Reader.ReadFromStream(AStream: TStream;
AParams: TsStreamParams = []);
APassword: String = ''; AParams: TsStreamParams = []);
var
OLEStream: TMemoryStream;
OLEStorage: TOLEStorage;
OLEDocument: TOLEDocument;
begin
Unused(AParams);
Unused(APassword, AParams);
OLEStream := TMemoryStream.Create;
try

View File

@ -126,7 +126,8 @@ type
procedure ReadXF(const AStream: TStream);
public
destructor Destroy; override;
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream;
APassword: String = ''; AParams: TsStreamParams = []); override;
end;
{ TsSpreadBIFF8Writer }
@ -958,13 +959,13 @@ begin
end;
procedure TsSpreadBIFF8Reader.ReadFromStream(AStream: TStream;
AParams: TsStreamParams = []);
APassword: String = ''; AParams: TsStreamParams = []);
var
OLEStream: TMemoryStream;
OLEStorage: TOLEStorage;
OLEDocument: TOLEDocument;
begin
Unused(AParams);
Unused(APassword, AParams);
OLEStream := TMemoryStream.Create;
try
// Only one stream is necessary for any number of worksheets

View File

@ -44,7 +44,7 @@ uses
fpszipper,
{$ENDIF}
fpsTypes, fpSpreadsheet, fpsUtils, fpsReaderWriter, fpsNumFormat, fpsPalette,
fpsxmlcommon, xlsCommon;
fpsxmlcommon, xlsCommon; //, xlsxdecrypter;
type
@ -105,7 +105,8 @@ type
public
constructor Create(AWorkbook: TsWorkbook); override;
destructor Destroy; override;
procedure ReadFromStream(AStream: TStream; AParams: TsStreamParams = []); override;
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); override;
end;
{ TsSpreadOOXMLWriter }
@ -2352,7 +2353,7 @@ begin
end;
procedure TsSpreadOOXMLReader.ReadFromStream(AStream: TStream;
AParams: TsStreamParams = []);
APassword: String = ''; AParams: TsStreamParams = []);
var
Doc : TXMLDocument;
RelsNode: TDOMNode;
@ -2374,7 +2375,7 @@ var
end;
begin
Unused(AParams);
Unused(APassword, AParams);
Doc := nil;
try

View File

@ -0,0 +1,596 @@
unit xlsxdecrypter;
{
Some of the ideas are aquired from http://www.lyquidity.com/devblog/?p=35
(the `internal` or `default password`): VelvetSweatshop
}
{$ifdef fpc}
{$mode objfpc}{$H+}
// {$mode delphi}
{$endif}
interface
uses
Classes
, SysUtils
, sha1
, DCPrijndael
;
const
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?!!
// EncryptionHeaderFlags as defined in 2.3.1 [MS-OFFCRYPTO]
ehfAES = $00000004;
//ehfExternal = $00000008;
//ehfDocProps = $00000010;
ehfCryptoAPI = $00000020;
// AlgorithmID
algRC4 = $00006801;
algAES128 = $0000660E;
algAES192 = $0000660F;
algAES256 = $00006610;
// HashID
hsSHA1 = $00008004;
// ProviderType
prRC4 = $00000001;
prAES = $00000018;
type
TVersion = packed record
Major : Word;
Minor : Word
end;
{ Defined in Section 2.3.2, 2.3.4.5 [MS-OFFCRYPTO] }
TEncryptionHeader = record
Flags : DWord; { defined in section 2.3.1 [MS-OFFCRYPTO] }
SizeExtra : DWord; { Must be equal to 0 }
AlgorithmID : DWord; { $00006801 -- RC4 }
{ $0000660E -- AES128}
{ $0000660F -- AES192}
{ $00006610 -- AES256}
HashID : DWord; { $00008004 -- SHA1 }
KeySize : DWord; { RC4 -- 40bits to 128bits (8-bit increments) }
{ AES128 -- 128 bits }
{ AES192 -- 192 bits }
{ AES256 -- 256 bits }
ProviderType: DWord; { $00000001 -- RC4 }
{ $00000018 -- AES }
Reserved1 : DWord; { Ignored }
Reserved2 : DWord; { Must be equal to 0 }
CSP_Name : string;
end;
{ Defined in Section 2.3.3 [MS-OFFCRYPTO] }
TEncryptionVerifier = record
SaltSize : DWord;
Salt : array[0..15] of Byte;
EncryptedVerifier : array[0..15] of Byte;
VerifierHashSize : DWord;
EncryptedVerifierHash: array[0..31] of Byte; // RC4 needs only 20 bytes
end;
// The EncryptionInfo Stream as define in 2.3.4.5 [MS-OFFCRYPTO]
TEncryptionInfo = record
Version : TVersion;
Flags : DWord;
HeaderSize: DWord;
Header : TEncryptionHeader;
Verifier : TEncryptionVerifier;
end;
{ TExcelFileDecryptor }
TExcelFileDecryptor = class
private
FEncInfo : TEncryptionInfo;
FEncryptionKey : TBytes;
// return empty string if everything done right otherwise the error message.
function InitEncryptionInfo(AStream: TStream): string;
//CheckPasswordInternal should be called after InitEncryptionInfo
function CheckPasswordInternal( APassword: UnicodeString ): Boolean;
public
// return empty string if everything done right otherwise the error message.
function Decrypt(inFileName: string; outStream: TStream): string; overload;
function Decrypt(inStream: TStream; outStream: TStream):string; overload;
// made this private because I don't know if it'll work with other passwords
function Decrypt(inFileName: string; outStream: TStream; APassword: UnicodeString): string; overload;
function Decrypt(inStream: TStream; outStream: TStream; APassword: UnicodeString): string; overload;
// return true if the password is correct.
function CheckPassword(AFileName: string; APassword: UnicodeString): Boolean;
function CheckPassword(AStream: TStream; APassword: UnicodeString): Boolean;
function isEncryptedAndSupported(AFileName: string): Boolean;
function isEncryptedAndSupported(AStream: TStream): Boolean;
end;
implementation
uses
fpolebasic
;
procedure ConcatToByteArray(var outArray: TBytes; Arr1: TBytes; Arr2: TBytes);
var
LenArr1 : Integer;
LenArr2 : Integer;
begin
LenArr1 := Length(Arr1);
LenArr2 := Length(Arr2);
SetLength( outArray, LenArr1 + LenArr2 );
if LenArr1 > 0 then
Move(Arr1[0], outArray[0], LenArr1);
if LenArr2 > 0 then
Move(Arr2[0], outArray[LenArr1], LenArr2);
end;
procedure ConcatToByteArray(var outArray: TBytes; AValue: DWord; Arr: TBytes);
var
LenArr : Integer;
begin
LenArr := Length(Arr);
SetLength( outArray, 4 + LenArr );
Move(AValue, outArray[0], 4);
if LenArr > 0 then
Move(Arr[0], outArray[4], LenArr);
end;
procedure ConcatToByteArray(var outArray: TBytes; Arr: TBytes; AValue: DWord);
var
LenArr : Integer;
begin
LenArr := Length(Arr);
SetLength( outArray, 4 + LenArr );
if LenArr > 0 then
Move(Arr[0], outArray[0], LenArr);
Move(AValue, outArray[LenArr], 4);
end;
function TExcelFileDecryptor.InitEncryptionInfo(AStream: TStream): string;
var
EncInfoStream: TMemoryStream;
OLEStorage: TOLEStorage;
OLEDocument: TOLEDocument;
FileSignature: QWord;
Pos : Int64;
Err : string;
begin
Err := '';
if not Assigned(AStream) then
Exit( 'Stream is null' );
AStream.Position := 0;
FileSignature := AStream.ReadQWord;
if FileSignature <> QWord(CFB_Signature) then
Exit( 'Wrong file signature' );
EncInfoStream := TMemoryStream.Create;
try
OLEStorage := TOLEStorage.Create;
try
OLEDocument.Stream := EncInfoStream;
AStream.Position := 0;
OLEStorage.ReadOLEStream(AStream, OLEDocument, 'EncryptionInfo');
if OLEDocument.Stream.Size = 0 then
raise Exception.Create('EncryptionInfo stream not found.');
EncInfoStream.Position := 0;
{ Major Version: $0002 = Excel 2003
$0003 = Excel 2007 | 2007 SP1
$0004 = Excel 2007 SP2 (not sure about 2010 | 2013) }
FEncInfo.Version.Major := EncInfoStream.ReadWord;
if (FEncInfo.Version.Major <> 3) and (FEncInfo.Version.Major <> 4) then
raise Exception.Create('File must be created with 2007 or 2010');
{ Minor Version: must be equal to $0002 }
FEncInfo.Version.Minor := EncInfoStream.ReadWord;
if FEncInfo.Version.Minor <> 2 then
raise Exception.Create('Incorrect File Version');
FEncInfo.Flags := EncInfoStream.ReadDWord;
FEncInfo.HeaderSize := EncInfoStream.ReadDWord;
///
/// ENCRYPTION HEADER
///
Pos := EncInfoStream.Position;
With FEncInfo.Header do
begin
Flags := EncInfoStream.ReadDWord;
if (Flags and ehfCryptoAPI) <> ehfCryptoAPI then
raise Exception.Create('File not encrypted');
if (Flags and ehfAES) <> ehfAES then
raise Exception.Create('Encryption must be AES');
SizeExtra := EncInfoStream.ReadDWord;
if SizeExtra <> 0 then
raise Exception.Create('Wrong Header.SizeExtra');
AlgorithmID := EncInfoStream.ReadDWord;
if ( AlgorithmID <> algAES128 )
and( AlgorithmID <> algAES192 )
and( AlgorithmID <> algAES256 )
//and( AlgorithmID <> algRC4 ) // not used by ECMA-376 format
then
raise Exception.Create('Unknown Encryption Algorithm');
HashID := EncInfoStream.ReadDWord;
if HashID <> hsSHA1 then
raise Exception.Create('Unknown Hashing Algorithm');
KeySize := EncInfoStream.ReadDWord;
if ( (AlgorithmID = algAES128) and (KeySize <> 128) )
or( (AlgorithmID = algAES192) and (KeySize <> 192) )
or( (AlgorithmID = algAES256) and (KeySize <> 256) )
//or( (AlgorithmID = algRC4) and (KeySize < 40 or KeySize > 128) )
then
raise Exception.Create('Incorrect Key Size');
ProviderType:= EncInfoStream.ReadDWord;
if ( ProviderType <> prAES )
//and( FEncInfo.Header.ProviderType <> prRC4 )
then
raise Exception.Create('Unknown Provider');
Reserved1 := EncInfoStream.ReadDWord;
Reserved2 := EncInfoStream.ReadDWord;
if Reserved2 <> 0 then
raise Exception.Create('Reserved2 must equal to 0');
//CSP_Name := Not needed
// CSP: Should be Microsoft Enhanced RSA and AES Cryptographic Provider
// or Microsoft Enhanced RSA and AES Cryptographic Provider (Prototype)
//Skip CSP Name
EncInfoStream.Position := Pos + FEncInfo.HeaderSize;
end;
///
/// ENCRYPTION VERIFIER
///
with FEncInfo.Verifier do
begin
SaltSize := EncInfoStream.ReadDWord;
if FEncInfo.Verifier.SaltSize <> 16 then
raise Exception.Create('Incorrect salt size');
EncInfoStream.ReadBuffer(Salt[0], SaltSize);
EncInfoStream.ReadBuffer(EncryptedVerifier[0], SaltSize);
VerifierHashSize := EncInfoStream.ReadDWord;
if FEncInfo.Header.ProviderType = prAES then
EncInfoStream.ReadBuffer( EncryptedVerifierHash[0], 32);
{ for RC4
else if FEncInfo.Header.ProviderType = prRC4 then
EncInfoStream.ReadBuffer( EncryptedVerifierHash[0], 20); }
end;
Err := '';
except
on E: Exception do
Err := E.Message;
end;
finally
if Assigned(OLEStorage) then
OLEStorage.Free;
EncInfoStream.Free;
end;
Result := Err;
end;
function TExcelFileDecryptor.CheckPasswordInternal(APassword: UnicodeString): Boolean;
var
AES_Cipher: TDCP_rijndael;
ConcArr : TBytes;
LastHash: TSHA1Digest;
Iterator, i: DWord;
X1_Buff: array[0..63] of byte;
X2_Buff: array[0..63] of byte;
X1_Hash: TSHA1Digest;
X2_Hash: TSHA1Digest;
EncryptionKeySize : Integer;
Verifier : array[0..15] of Byte;
VerifierHash: array[0..31] of Byte;// Needs only 20bytes to hold the SHA1
// but needs 32bytes to hold the decrypted hash
begin
// if no password used, use microsoft default.
if APassword = '' then
APassword := 'VelvetSweatshop';
//// [MS-OFFCRYPTO]
//// 2.3.4.7 ECMA-376 Document Encryption Key Generation
// 1.1.Concat Salt and Password
// Calculate SHA1(0) = SHA1(salt + password)
ConcatToByteArray( ConcArr
, FEncInfo.Verifier.Salt
, TEncoding.Unicode.GetBytes(APassword));
LastHash := SHA1Buffer( ConcArr[0], Length(ConcArr) );
// 1.2.Calculate SHA1(n) = SHA1(iterator + SHA1(n-1) ) -- iterator is 32bit
for Iterator := 0 to 49999 do
begin
ConcatToByteArray(ConcArr, Iterator, LastHash);
LastHash := SHA1Buffer( ConcArr[0], Length(ConcArr) );
end;
// 1.3.Claculate final hash, SHA1(final) = SHA1(H(n) + block) -- block = 0 (32bit)
ConcatToByteArray(ConcArr, LastHash, 0);
LastHash := SHA1Buffer( ConcArr[0], Length(ConcArr) );
//// 2.Derive the encryption key.
// 2.1 cbRequiredKeyLength for AES is 128,192,256bit ?!!! must be < 40bytes
// 2.2 cbHash = 20bytes ( 160bit),, length of SHA1 hash
// 2.3 + 2.4 Claculate X1 and X2 the SHA of the generated 64bit Arrays.
// FillByte(X1_Buff[0], 64, $36);
// FillByte(X2_Buff[0], 64, $5C);
for i := 0 to 19 do
begin
X1_Buff[i] := LastHash[i] xor $36;
X2_Buff[i] := LastHash[i] xor $5C;
end;
for i := 20 to 63 do
begin
X1_Buff[i] := $36;
X2_Buff[i] := $5C;
end;
X1_Hash := SHA1Buffer( X1_Buff[0], Length(X1_Buff) );
X2_Hash := SHA1Buffer( X2_Buff[0], Length(X2_Buff) );
// 2.5 Concat X1, X2 -> X3 = X1 + X2 (X3 = 40bytes in length)
//ConcatToByteArray( ConcArr, X1_Hash, X2_Hash );
// 2.6 Let keyDerived be equal to the first cbRequiredKeyLength bytes of X3.
// We'll fill the Encryption key on the fly, so we won't need X3
// This Key (FEncryptionKey) is used for decryption method
EncryptionKeySize := FEncInfo.Header.KeySize div 8; // Convert Size from bits to bytes
SetLength(FEncryptionKey, EncryptionKeySize);
if EncryptionKeySize <= 20 then
begin
Move(X1_Hash[0], FEncryptionKey[0], EncryptionKeySize);
end
else
begin
Move(X1_Hash[0], FEncryptionKey[0], EncryptionKeySize);
Move(X2_Hash[0], FEncryptionKey[20], EncryptionKeySize-20);
end;
//// 2.3.4.9 Password Verification
// 1. Encryption key is FEncryptionKey
// 2. Decrypt the EncryptedVerifier
AES_Cipher := TDCP_rijndael.Create(nil);
AES_Cipher.Init( FEncryptionKey[0], FEncInfo.Header.KeySize, nil );
AES_Cipher.DecryptECB(FEncInfo.Verifier.EncryptedVerifier[0] , Verifier[0]);
// 3. Decrypt the DecryptedVerifierHash
AES_Cipher.Burn;
AES_Cipher.Init( FEncryptionKey[0], FEncInfo.Header.KeySize, nil );
AES_Cipher.DecryptECB(FEncInfo.Verifier.EncryptedVerifierHash[0] , VerifierHash[0]);
AES_Cipher.DecryptECB(FEncInfo.Verifier.EncryptedVerifierHash[16], VerifierHash[16]);
AES_Cipher.Free;
// 4. Calculate SHA1(Verifier)
LastHash := SHA1Buffer(Verifier[0], Length(Verifier));
// 5. Compare results
Result := (CompareByte( LastHash[0], VerifierHash[0], 20) = 0);
end;
function TExcelFileDecryptor.Decrypt(inFileName: string; outStream: TStream
): string;
begin
Result := Decrypt(inFileName, outStream, 'VelvetSweatshop' );
end;
function TExcelFileDecryptor.Decrypt(inFileName: string; outStream: TStream;
APassword: UnicodeString): string;
Var
inStream : TFileStream;
begin
if not FileExists(inFileName) then
Exit( inFileName + ' not found.' );
try
inStream := TFileStream.Create( inFileName, fmOpenRead );
inStream.Position := 0;
Result := Decrypt( inStream, outStream, APassword );
finally
inStream.Free;
end;
end;
function TExcelFileDecryptor.Decrypt(inStream: TStream; outStream: TStream
): string;
begin
Result := Decrypt(inStream, outStream, 'VelvetSweatshop' );
end;
function TExcelFileDecryptor.Decrypt(inStream: TStream; outStream: TStream;
APassword: UnicodeString): string;
var
OLEStream: TMemoryStream;
OLEStorage: TOLEStorage;
OLEDocument: TOLEDocument;
AES_Cipher : TDCP_rijndael;
inData : TBytes;
outData : TBytes;
StreamSize : QWord;
KeySizeByte: Integer;
Err : string;
begin
if (not Assigned(inStream)) or (not Assigned(outStream)) then
Exit( 'streams must be assigned' );
Err := InitEncryptionInfo(inStream);
if Err <> '' then
Exit( 'Error when initializing Encryption Info'#10#13 + Err );
if not CheckPasswordInternal(APassword) then
Exit( 'Wrong password' );
// read the encoded stream into memory
OLEStream := TMemoryStream.Create;
try
OLEStorage := TOLEStorage.Create;
try
OLEDocument.Stream := OLEStream;
inStream.Position := 0;
OLEStorage.ReadOLEStream(inStream, OLEDocument, 'EncryptedPackage');
if OLEDocument.Stream.Size = 0 then
raise Exception.Create('EncryptedPackage stream not found.');
// Start decryption
OLEStream.Position:=0;
outStream.Position:=0;
StreamSize := OLEStream.ReadQWord;
KeySizeByte := FEncInfo.Header.KeySize div 8;
SetLength(inData, KeySizeByte);
SetLength(outData, KeySizeByte);
AES_Cipher := TDCP_rijndael.Create(nil);
AES_Cipher.Init( FEncryptionKey[0], FEncInfo.Header.KeySize, nil );
While StreamSize > 0 do
begin
OLEStream.ReadBuffer(inData[0], KeySizeByte);
AES_Cipher.DecryptECB(inData[0], outData[0]);
if StreamSize < KeySizeByte then
outStream.WriteBuffer(outData[0], StreamSize) // Last block less then key size
else
outStream.WriteBuffer(outData[0], KeySizeByte);
if StreamSize < KeySizeByte then
StreamSize := 0
else
Dec(StreamSize, KeySizeByte);
end;
AES_Cipher.Free;
/////
except
Err := 'EncryptedPackage not found';
end;
finally
if Assigned(OLEStorage) then
OLEStorage.Free;
OLEStream.Free;
end;
Exit( Err );
end;
function TExcelFileDecryptor.isEncryptedAndSupported(AFileName: string
): Boolean;
var
AStream : TStream;
begin
if not FileExists(AFileName) then
Exit( False );
try
AStream := TFileStream.Create( AFileName, fmOpenRead );
AStream.Position := 0;
//FStream.CopyFrom(AStream, AStream.Size);
Result := isEncryptedAndSupported( AStream );
finally
AStream.Free;
end;
end;
function TExcelFileDecryptor.isEncryptedAndSupported(AStream: TStream
): Boolean;
begin
if not Assigned(AStream) then
Exit( False );
if InitEncryptionInfo(AStream) <> '' then
Exit( False );
Result := True;
end;
function TExcelFileDecryptor.CheckPassword(AFileName: string;
APassword: UnicodeString): Boolean;
var
AStream : TStream;
begin
if not FileExists(AFileName) then
Exit( False );
try
AStream := TFileStream.Create( AFileName, fmOpenRead );
AStream.Position := 0;
Result := CheckPassword( AStream, APassword );
finally
AStream.Free;
end;
end;
function TExcelFileDecryptor.CheckPassword(AStream: TStream;
APassword: UnicodeString): Boolean;
begin
if not Assigned(AStream) then
Exit( False );
AStream.Position := 0;
if InitEncryptionInfo(AStream) <> '' then
Exit( False );
Result := CheckPasswordInternal(APassword);
end;
end.

View File

@ -0,0 +1,68 @@
unit xlsxooxml_crypto;
interface
uses
Classes,
fpstypes, xlsxooxml, xlsxdecrypter;
type
TsSpreadOOXMLReaderCrypto = class(TsSpreadOOXMLReader)
public
procedure ReadFromStream(AStream: TStream; APassword: String = '';
AParams: TsStreamParams = []); override;
end;
var
sfidOOXML_Crypto: TsSpreadFormatID;
implementation
uses
fpsReaderWriter;
procedure TsSpreadOOXMLReaderCrypto.ReadFromStream(AStream: TStream;
APassword: String = ''; AParams: TsStreamParams = []);
var
ExcelDecrypt : TExcelFileDecryptor;
DecryptedStream: TStream;
begin
ExcelDecrypt := TExcelFileDecryptor.Create;
try
AStream.Position := 0;
if ExcelDecrypt.isEncryptedAndSupported(AStream) then
begin
DecryptedStream := TMemoryStream.Create;
try
ExcelDecrypt.Decrypt(AStream, DecryptedStream, APassword);
// Discard encrypted stream and load decrypted one.
AStream.Free;
AStream := TMemoryStream.Create;
DecryptedStream.Position := 0;
AStream.CopyFrom(DecryptedStream, DecryptedStream.Size);
AStream.Position := 0;
finally
DecryptedStream.Free;
end;
end;
finally
ExcelDecrypt.Free;
AStream.Position := 0;
end;
inherited;
end;
initialization
// Registers this reader/writer for fpSpreadsheet
sfidOOXML_Crypto := RegisterSpreadFormat(sfUser,
TsSpreadOOXMLReaderCrypto, nil,
STR_FILEFORMAT_EXCEL_XLSX, 'OOXML', [STR_OOXML_EXCEL_EXTENSION, '.xlsm']
);
end.
end.

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<CONFIG>
<Package Version="4">
<PathDelim Value="\"/>
<Name Value="laz_fpspreadsheet_crypto"/>
<Type Value="RunTimeOnly"/>
<CompilerOptions>
<Version Value="11"/>
<PathDelim Value="\"/>
<SearchPaths>
<OtherUnitFiles Value="crypto"/>
<UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)\"/>
</SearchPaths>
</CompilerOptions>
<Description Value="Encryption / decryption support for FPSpreadsheet"/>
<Version Major="1" Minor="9"/>
<Files Count="2">
<Item1>
<Filename Value="crypto\xlsxdecrypter.pas"/>
<UnitName Value="xlsxdecrypter"/>
</Item1>
<Item2>
<Filename Value="crypto\xlsxooxml_crypto.pas"/>
<UnitName Value="xlsxooxml_crypto"/>
</Item2>
</Files>
<RequiredPkgs Count="3">
<Item1>
<PackageName Value="dcpcrypt"/>
</Item1>
<Item2>
<PackageName Value="laz_fpspreadsheet"/>
</Item2>
<Item3>
<PackageName Value="FCL"/>
</Item3>
</RequiredPkgs>
<UsageOptions>
<UnitPath Value="$(PkgOutDir)"/>
</UsageOptions>
<PublishOptions>
<Version Value="2"/>
</PublishOptions>
</Package>
</CONFIG>