fpspreadsheet: Add specialized reader for password-protected ods files. Temporarily add dependence on Abbrevia to crypto package (until FPC issue #40369 will be fixed). Add sample project for reading encrypted ods file.

git-svn-id: https://svn.code.sf.net/p/lazarus-ccr/svn@8910 8e941d3f-bd1b-0410-a28a-d453659cc2b4
This commit is contained in:
wp_xxyyzz
2023-08-03 12:58:43 +00:00
parent 1f0a5d6513
commit 36537f3875
9 changed files with 552 additions and 85 deletions

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<CONFIG>
<ProjectOptions>
<Version Value="12"/>
<PathDelim Value="\"/>
<General>
<Flags>
<MainUnitHasCreateFormStatements Value="False"/>
<MainUnitHasTitleStatement Value="False"/>
<MainUnitHasScaledStatement Value="False"/>
</Flags>
<SessionStorage Value="InProjectDir"/>
<Title Value="read_encrypted_ods"/>
<UseAppBundle Value="False"/>
<ResourceType Value="res"/>
</General>
<BuildModes>
<Item Name="Default" Default="True"/>
</BuildModes>
<PublishOptions>
<Version Value="2"/>
<UseFileFilters Value="True"/>
</PublishOptions>
<RunParams>
<FormatVersion Value="2"/>
</RunParams>
<RequiredPackages>
<Item>
<PackageName Value="laz_fpspreadsheet_crypto"/>
</Item>
<Item>
<PackageName Value="laz_fpspreadsheet"/>
</Item>
</RequiredPackages>
<Units>
<Unit>
<Filename Value="read_encrypted_ods.lpr"/>
<IsPartOfProject Value="True"/>
</Unit>
</Units>
</ProjectOptions>
<CompilerOptions>
<Version Value="11"/>
<PathDelim Value="\"/>
<Target>
<Filename Value="read_encrypted_ods"/>
</Target>
<SearchPaths>
<IncludeFiles Value="$(ProjOutDir)"/>
<UnitOutputDirectory Value="lib\$(TargetCPU)-$(TargetOS)"/>
</SearchPaths>
<Linking>
<Debugging>
<DebugInfoType Value="dsDwarf3"/>
</Debugging>
</Linking>
</CompilerOptions>
<Debugging>
<Exceptions>
<Item>
<Name Value="EAbort"/>
</Item>
<Item>
<Name Value="ECodetoolError"/>
</Item>
<Item>
<Name Value="EFOpenError"/>
</Item>
</Exceptions>
</Debugging>
</CONFIG>

View File

@@ -0,0 +1,36 @@
program read_encrypted_ods;
uses
SysUtils,
fpSpreadsheet, fpsTypes, fpsUtils, fpsOpenDocument_Crypto;
const
FILENAME = 'pwd 123.ods';
PASSWORD = '123';
var
wb: TsWorkbook;
ws: TsWorksheet;
cell: PCell;
fn: String;
fmtID: TsSpreadFormatID;
t: TDateTime;
begin
t := Now;
wb := TsWorkbook.Create;
try
wb.ReadFromFile(FILENAME, sfidOpenDocument_Crypto, PASSWORD, []);
ws := wb.GetFirstWorksheet;
if ws <> nil then
for cell in ws.Cells do
WriteLn('cell ', GetCellString(cell^.Row, cell^.Col), ' = "', ws.ReadAsText(cell), '"');
finally
wb.Free;
end;
t := Now - t;
WriteLn('Time to decrypt and load: ', FormatDateTime('nn:ss.zzz', t), ' seconds');
Write('Press ENTER to close...');
ReadLn;
end.

View File

@@ -14,7 +14,7 @@
</CompilerOptions>
<Description Value="Encryption / decryption support for FPSpreadsheet"/>
<Version Major="1" Minor="14"/>
<Files Count="3">
<Files Count="4">
<Item1>
<Filename Value="source\crypto\xlsxdecrypter.pas"/>
<UnitName Value="xlsxdecrypter"/>
@@ -25,19 +25,26 @@
</Item2>
<Item3>
<Filename Value="source\crypto\fpscryptoproc.pas"/>
<UnitName Value="fpscryptoproc"/>
<UnitName Value="fpsCryptoProc"/>
</Item3>
<Item4>
<Filename Value="source\crypto\fpsopendocument_crypto.pas"/>
<UnitName Value="fpsOpenDocument_Crypto"/>
</Item4>
</Files>
<CompatibilityMode Value="True"/>
<RequiredPkgs Count="2">
<RequiredPkgs Count="3">
<Item1>
<PackageName Value="Abbrevia"/>
</Item1>
<Item2>
<PackageName Value="laz_fpspreadsheet"/>
<MaxVersion Major="1" Minor="11"/>
<MinVersion Major="1" Minor="12" Valid="True"/>
</Item1>
<Item2>
<PackageName Value="FCL"/>
</Item2>
<Item3>
<PackageName Value="FCL"/>
</Item3>
</RequiredPkgs>
<UsageOptions>
<UnitPath Value="$(PkgOutDir)"/>

View File

@@ -83,11 +83,11 @@ type
FileName: String;
Encrypted: Boolean;
// Checksum of encrypted data
EncryptionData_Checksum: String;
EncryptionData_Checksum: string; // base64
EncryptionData_ChecksumType: String;
// Encrypting the data with the pwd hash ("key")
AlgorithmName: String;
InitializationVector: RawByteString;
InitializationVector: string; // base64
IterationCount: Integer;
// Start key generation
StartKeyGenerationName: String;
@@ -95,7 +95,7 @@ type
// Key derivation (encrypting and salting the start key)
KeyDerivationName: String;
KeySize: Integer;
Salt: RawByteString;
Salt: string; // base64
end;
{ TsSpreadOpenDocReader }
@@ -202,12 +202,12 @@ type
ACellNode: TDOMNode); reintroduce;
function Decrypt(AStream: TStream; ADecryptionInfo: TsOpenDocManifestFileEntry;
APassword: RawByteString; ADestStream: TStream): Boolean; virtual;
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;
APassword: RawByteString; ADestStream: TStream): Boolean;
APassword: RawByteString; ADestStream: TStream; out AErrorMsg: String): Boolean;
public
constructor Create(AWorkbook: TsBasicWorkbook); override;
destructor Destroy; override;
@@ -2897,6 +2897,7 @@ var
sheet: TsWorksheet;
sheetName: String;
tablestyleName: String;
err: String;
isEncrypted: Boolean = false;
function CreateXMLStream: TStream;
@@ -2939,8 +2940,10 @@ begin
// process the styles.xml file
XMLStream := CreateXMLStream;
try
if UnzipToStream(AStream, 'styles.xml', APassword, XMLStream) then
ReadXMLStream(Doc, XMLStream);
if UnzipToStream(AStream, 'styles.xml', APassword, XMLStream, err) then
ReadXMLStream(Doc, XMLStream)
else
raise EFPSpreadsheetReader.Create(err);
finally
XMLStream.Free;
end;
@@ -2959,10 +2962,10 @@ begin
// process the content.xml file
XMLStream := CreateXMLStream;
try
if UnzipToStream(AStream, 'content.xml', APassword, XMLStream) then
if UnzipToStream(AStream, 'content.xml', APassword, XMLStream, err) then
ReadXMLStream(Doc, XMLStream)
else
raise EFPSpreadsheetReader.CreateFmt(rsDefectiveInternalFileStructure, ['ods']);
raise EFPSpreadsheetReader.Create(err);
finally
XMLStream.Free;
end;
@@ -3050,7 +3053,7 @@ begin
// process the meta.xml file
XMLStream := CreateXMLStream;
try
if UnzipToStream(AStream, 'meta.xml', APassword, XMLStream) then
if UnzipToStream(AStream, 'meta.xml', APassword, XMLStream, err) then
begin
ReadXMLStream(Doc, XMLStream);
try
@@ -3058,7 +3061,8 @@ begin
finally
FreeAndNil(Doc);
end;
end;
end else
raise EFPSpreadsheetReader.Create(err);
finally
XMLStream.Free;
end;
@@ -3066,7 +3070,7 @@ begin
// process the settings.xml file (Note: it does not always exist!)
XMLStream := CreateXMLStream;
try
if UnzipToStream(AStream, 'settings.xml', APassword, XMLStream) then
if UnzipToStream(AStream, 'settings.xml', APassword, XMLStream, err) then
begin
ReadXMLStream(Doc, XMLStream);
try
@@ -3075,7 +3079,8 @@ begin
finally
FreeAndNil(Doc);
end;
end;
end else
raise EFPSpreadsheetReader.Create(err);
finally
XMLStream.Free;
end;
@@ -5601,8 +5606,8 @@ end;
descendant reader class which implements decryption taking advantage of the
provided password and the parameters in ADecryptionInfo. }
function TsSpreadOpenDocReader.Decrypt(AStream: TStream;
ADecryptionInfo: TsOpenDocManifestFileEntry; APassword: RawByteString;
ADestStream: TStream): Boolean;
ADecryptionInfo: TsOpenDocManifestFileEntry; APassword: String;
ADestStream: TStream; out AErrorMsg: String): Boolean;
begin
Result := false;
end;
@@ -5621,13 +5626,14 @@ begin
end;
function TsSpreadOpenDocReader.UnzipToStream(AStream: TStream; AZippedFile: String;
APassword: RawByteString; ADestStream: TStream): Boolean;
APassword: RawByteString; ADestStream: TStream; out AErrorMsg: String): Boolean;
var
i: Integer;
mfe: TsOpenDocManifestFileEntry;
isEncrypted: Boolean;
tmpStream: TStream;
begin
AErrorMsg := '';
mfe := nil;
// Find the requested file among the manifest file entries containing
@@ -5646,7 +5652,7 @@ begin
// Read the encrypted file from the input stream
Result := UnzipToStream(AStream, AZippedFile, tmpStream);
// Decrypt the file into the destination stream
Result := Result and Decrypt(tmpStream, mfe, APassword, ADestStream);
Result := Result and Decrypt(tmpStream, mfe, APassword, ADestStream, AErrorMsg);
finally
tmpStream.Free;
end;

View File

@@ -202,6 +202,8 @@ function SplitStr(const AText: String; ADelimiter: Char): TStringArray;
function SafeQuoteStr(AString: String): String;
function UnquoteStr(AString: String): String;
function StringToBytes(const AString: String): TBytes;
function InitSearchParams(ASearchText: String = ''; AOptions: TsSearchOptions = [];
ASearchWithin: TsSearchWithin = swWorksheet): TsSearchParams;
function InitReplaceParams(AReplaceText: String = '';
@@ -2481,6 +2483,18 @@ begin
end;
end;
{@@ ----------------------------------------------------------------------------
Copies a string to a TBytes variable
@@param AString The string to be copied
@@returns A dynamic array of byte, type TBytes
-------------------------------------------------------------------------------}
function StringToBytes(const AString: String): TBytes;
begin
SetLength(Result, Length(AString));
Move(AString[1], Result[0], Length(AString));
end;
{@@ ----------------------------------------------------------------------------
Initializes a SearchParams record. This record defines the parameters needed
when searching cells.

View File

@@ -35,12 +35,24 @@ uses
aes_type, aes_cbc, aes_ecb;
{$ENDIF}
{$IFDEF DCPCRYPT}
DCPrijndael;
DCPcrypt2, DCPrijndael;
{$ENDIF}
function Calc_SHA1(const AText: RawByteString): RawByteString;
function Calc_SHA256(AText: RawByteString): RawByteString;
function PBKDF2_HMAC_SHA1(pass, salt: RawByteString; count, kLen: Integer): RawByteString;
function ConcatToByteArray(const InArray1, InArray2: TBytes): TBytes;
procedure ConcatToByteArray(var OutArray: TBytes;
Ptr1: PByte; ACount1: Integer; Ptr2: PByte; ACount2: Integer);
procedure ConcatToByteArray(var OutArray: TBytes;
Ptr: PByte; ACount: Integer; const Arr: TBytes);
function DecodeBase64(const AString: String): TBytes;
function Calc_SHA1(const Buf; BufSize: LongWord): TBytes;
function Calc_SHA1(Buf: TBytes): TBytes;
function Calc_SHA256(const Buf; BufSize: LongWord): TBytes;
function Calc_SHA256(Buf: TBytes): TBytes;
function PBKDF2_HMAC_SHA1(pass, salt: TBytes; count, kLen: Integer): TBytes;
function Decrypt_AES_ECB(const Key; KeySizeBits: LongWord;
const InData; var OutData; DataSize: LongWord): String;
@@ -48,115 +60,161 @@ function Decrypt_AES_ECB(const Key; KeySizeBits: LongWord;
function DecryptStream_AES_ECB(const Key; KeySizeBits: LongWord;
ASrcStream, ADestStream: TStream; ASrcStreamSize: QWord): String;
function Decrypt_AES_CBC(const Key; KeySizeBits: LongWord; InitVector: Pointer;
ASrcStream, ADestStream: TStream): String;
function VerifyDecrypt(AStream: TStream; CheckSum, ChecksumType: string): boolean;
implementation
uses
Math;
Math, fpsUtils;
function Calc_SHA1(const AText: RawByteString): RawByteString;
var
sha1Digest: TSHA1Digest;
function ConcatToByteArray(const InArray1, InArray2: TBytes): TBytes;
begin
sha1Digest := SHA1String(AText);
SetLength(Result, 20);
Move(sha1Digest[0], Result[1], 20);
ConcatToByteArray(Result, @InArray1[0], Length(InArray1), @InArray2[0], Length(InArray2));
end;
function Calc_SHA256(AText: RawByteString): RawByteString;
procedure ConcatToByteArray(var OutArray: TBytes; Ptr1: PByte; ACount1: Integer;
Ptr2: PByte; ACount2: Integer);
begin
SetLength(OutArray, ACount1 + ACount2);
if ACount1 > 0 then
Move(Ptr1^, OutArray[0], ACount1);
if ACount2 > 0 then
Move(Ptr2^, OutArray[ACount1], ACount2);
end;
procedure ConcatToByteArray(var OutArray: TBytes; Ptr: PByte; ACount: Integer;
const Arr: TBytes);
begin
ConcatToByteArray(OutArray, Ptr, ACount, @Arr[0], Length(Arr));
end;
function DecodeBase64(const AString: String): TBytes;
begin
Result := StringToBytes(DecodeStringBase64(AString));
end;
function Calc_SHA1(const Buf; BufSize: LongWord): TBytes;
var
digest: TSHA1Digest;
begin
digest := SHA1Buffer(Buf, BufSize);
SetLength(Result, Length(digest));
Move(digest[0], Result[0], Length(digest));
end;
function Calc_SHA1(Buf: TBytes): TBytes;
begin
Result := Calc_SHA1(Buf[0], Length(Buf));
end;
function Calc_SHA256(const Buf; BufSize: LongWord): TBytes;
var
sha256: TSHA256;
begin
sha256.Init;
sha256.Update(@AText[1], Length(AText));
sha256.Update(@Buf, BufSize);
sha256.Final;
SetLength(Result, 32);
Move(sha256.Digest[0], Result[1], 32);
SetLength(Result, SizeOf(TSHA256Digest));
Move(sha256.Digest[0], Result[0], SizeOf(TSHA256Digest));
end;
function RPad(x: RawByteString; c: Char; s: Integer): RawByteString;
function Calc_SHA256(Buf: TBytes): TBytes;
begin
Result := Calc_SHA256(Buf[0], Length(Buf));
end;
function RPad(Data: TBytes; PadByte: Byte; ALen: Integer): TBytes;
var
L: Integer;
begin
L := Length(x);
if L < s then
L := Length(Data);
if L < ALen then
begin
SetLength(Result, s);
Move(x[1], Result[1], L);
FillChar(Result[L+1], s-L, c);
SetLength(Result, ALen);
Move(Data[0], Result[0], L);
FillChar(Result[L], ALen-L, PadByte);
end else
Result := x;
Result := Data;
end;
function Fill(c: Char; Len: Integer): RawByteString; inline;
function Fill(b: Byte; Len: Integer): TBytes; inline;
begin
SetLength(Result, Len);
FillChar(Result[1], Len, c);
FillChar(Result[0], Len, b);
end;
function XorBlock(s, x: RawByteString): RawByteString; inline;
function XorBlock(s, x: TBytes): TBytes; inline;
var
L, i: Integer;
Ps, Px: PByte;
begin
L := Length(s);
SetLength(Result, L);
Ps := PByte(@s[1]);
Px := PByte(@x[1]);
for i := 1 to L do
Ps := PByte(@s[0]);
Px := PByte(@x[0]);
for i := 0 to L-1 do
begin
Result[i] := Char(Ps^ xor Px^);
Result[i] := Ps^ xor Px^;
inc(Ps);
inc(Px);
end;
end;
function Calc_HMAC_SHA1(message, key: RawByteString): RawByteString;
function Calc_HMAC_SHA1(message, key: TBytes): TBytes;
const
blockSize = 64;
begin
if Length(key) > blocksize then
key := Calc_SHA1(key);
key := RPad(key, #0, blocksize);
key := RPad(key, 0, blocksize);
Result := Calc_SHA1(XorBlock(key, Fill(#$36, blocksize)) + message);
Result := Calc_SHA1(XorBlock(key, Fill(#$5c, blocksize)) + Result);
Result := Calc_SHA1(ConcatToByteArray(XorBlock(key, Fill($36, blocksize)), message));
Result := Calc_SHA1(ConcatToByteArray(XorBlock(key, Fill($5c, blockSize)), Result));
//Result := Calc_SHA1(XorBlock(key, Fill($36, blocksize)) + message);
//Result := Calc_SHA1(XorBlock(key, Fill($5c, blocksize)) + Result);
end;
// https://keit.co/dcpcrypt-hmac-rfc2104/
function PBKDF2_HMAC_SHA1(pass, salt: RawByteString; count, kLen: Integer): RawByteString;
function PBKDF2_HMAC_SHA1(pass, salt: TBytes; count, kLen: Integer): TBytes;
function IntX(i: Integer): RawByteString;
function IntX(i: LongInt): TBytes;
type
Int4 = record
i24, i16, i8, i0: char;
i24, i16, i8, i0: byte;
end;
begin
SetLength(Result, 4);
Result[1] := Int4(i).i0;
Result[2] := Int4(i).i8;
Result[3] := Int4(i).i16;
Result[4] := Int4(i).i24;
Result[0] := Int4(i).i0;
Result[1] := Int4(i).i8;
Result[2] := Int4(i).i16;
Result[3] := Int4(i).i24;
end;
var
D, I, J: Integer;
T, F, U: RawByteString;
T, F, U: TBytes;
begin
T := '';
T := nil;
D := Ceil(kLen / 20); //(hash.GetHashSize div 8));
for i := 1 to D do
begin
F := Calc_HMAC_SHA1(salt + IntX(i), pass);
F := Calc_HMAC_SHA1(ConcatToByteArray(salt, IntX(i)), pass);
U := F;
for j := 2 to count do
begin
U := Calc_HMAC_SHA1(U, pass);
F := XorBlock(F, U);
end;
T := T + F;
T := ConcatToByteArray(T, F); // T := T + F;
end;
Result := Copy(T, 1, kLen);
Result := nil;
SetLength(Result, kLen);
Move(T[0], Result[0], kLen);
// Result := Copy(T, 1, kLen);
end;
function Decrypt_AES_ECB(const Key; KeySizeBits: LongWord;
@@ -252,5 +310,105 @@ begin
{$ENDIF}
end;
{ Decrypts the data in the stream ASrcStream and stores the result in the
stream ADestStream.
Decryption algorithm is AES method CBC.
The hashed password is provided as parameter Key, its length in bits is
given by KeySizeBits.
The initialization vector is specified in InitVector.
If an error occurs, the function result returns an error message with
error code (error codes are listed in unit AES_Type). Otherwise the
function result is an empty string. }
function Decrypt_AES_CBC(const Key; KeySizeBits: LongWord; InitVector: Pointer;
ASrcStream, ADestStream: TStream): String;
{$IFDEF WOLFGANG_EHRHARDT_LIB}
const
BUF_SIZE = $4000; {must be a multiple of AESBLKSIZE=16 for CBC}
var
ctx: TAESContext;
buffer: array[0..BUF_SIZE-1] of byte;
len, err: Integer;
n: Word;
begin
Result := '';
err := AES_CBC_Init_Decr(Key, KeySizebits, PAESBlock(InitVector)^, ctx{%H-});
if err <> 0 then
begin
Result := 'Decrypt init error ' + IntToStr(err);
exit;
end;
len := ASrcStream.Size;
while len > 0 do
begin
if len > SizeOf(buffer) then
n := SizeOf(buffer)
else
n := len;
ASrcStream.Read(buffer{%H-}, n);
dec(len, n);
err := AES_CBC_Decrypt(@buffer, @buffer, n, ctx);
if err <> 0 then
begin
Result := 'Decrypt error: ' + IntToStr(err);
exit;
end;
ADestStream.Write(buffer, n);
end;
end;
{$ENDIF}
{$IFDEF DCPCRYPT}
var
AES_cipher: TDCP_rijndael;
begin
AES_cipher := TDCP_rijndael.Create(nil);
try
AES_cipher.Init(Key, KeySizebits, InitVector);
AES_cipher.CipherMode := cmCBC;
AES_cipher.DecryptStream(ASrcStream, ADestStream, ASrcStream.Size);
finally
AES_cipher.Free;
end;
end;
{$ENDIF}
function VerifyDecrypt(AStream: TStream; CheckSum, ChecksumType: string): boolean;
var
p: Int64;
buffer: Array of byte = nil;
expCheckSum: TBytes;
currCheckSumSHA1: TSHA1Digest;
lSHA256: TSHA256;
currChecksumSHA256: TSHA256Digest;
begin
Result := false;
p := AStream.Position;
expCheckSum := DecodeBase64(CheckSum);
case Uppercase(ChecksumType) of
'SHA1/1K', 'SHA1-1K':
begin
SetLength(buffer, 1024);
AStream.Write(buffer[0], Length(buffer));
currCheckSumSHA1 := SHA1Buffer(buffer[0], 1024);
if Length(expCheckSum) = Length(TSHA1Digest) then
Result := CompareMem(@expCheckSum[0], @currCheckSumSHA1[0], Length(TSHA1Digest));
end;
'SHA256/1K', 'SHA256-1K':
begin
SetLength(buffer, 1024);
AStream.Read(buffer[0], Length(buffer));
lSHA256.Init;
lSHA256.Update(@buffer[0], Length(Buffer));
lSHA256.Final;
currCheckSumSHA256 := lSHA256.Digest;
if Length(expCheckSum) = Length(TSHA256Digest) then
Result := CompareMem(@expChecksum[0], @currCheckSumSHA256[0], Length(TSHA256Digest));
end;
end;
AStream.Position := p;
end;
end.

View File

@@ -0,0 +1,192 @@
unit fpsOpenDocument_Crypto;
{$MODE ObjFPC}{$H+}
{$DEFINE UNZIP_ABBREVIA} // Remove this define when zipper is fixed.
interface
uses
Classes, SysUtils,
{$IFDEF UNZIP_ABBREVIA}
ABUnzper,
{$ENDIF}
fpsTypes, fpsOpenDocument;
type
TsSpreadOpenDocReaderCrypto = class(TsSpreadOpenDocReader)
private
function CalcPasswordHash(ADecryptionInfo: TsOpenDocManifestFileEntry;
APassword: String): TBytes;
protected
function Decrypt(AStream: TStream; ADecryptionInfo: TsOpenDocManifestFileEntry;
APassword: String; ADestStream: TStream; out AErrorMsg: String): Boolean; override;
function SupportsDecryption: Boolean; override;
{$IFDEF UNZIP_ABBREVIA}
function UnzipToStream(AStream: TStream; AZippedFile: String; ADestStream: TStream): Boolean; override;
{$ENDIF}
end;
var
sfidOpenDocument_Crypto: TsSpreadFormatID;
implementation
uses
zStream,
fpsReaderWriter, fpsCryptoProc;
{ Decompresses the source stream and stored the output in the destination stream. }
procedure Decompress(ASrcStream, ADestStream: TStream);
var
decompressor: TDecompressionStream;
begin
decompressor := TDecompressionStream.Create(ASrcStream, true);
try
ADestStream.CopyFrom(decompressor, 0); // 0 --> entire src stream
finally
decompressor.Free;
end;
end;
{-------------------------------------------------------------------------------
TsSpreadOpenDocReaderCrypto
-------------------------------------------------------------------------------}
{ AStream contains one encrypted xml file of the ods file structure. The method
decrypts the stream based on the information provided in ADecryptionInfo and
using the given (unhashed) user password. The output is stored in the
destination stream. }
function TsSpreadOpenDocReaderCrypto.Decrypt(AStream: TStream;
ADecryptionInfo: TsOpenDocManifestFileEntry; APassword: String;
ADestStream: TStream; out AErrorMsg: String): Boolean;
var
pwdHash: TBytes;
iv: TBytes;
tmpStream: TStream;
algorithm: String;
begin
Result := false;
algorithm := LowerCase(ADecryptionInfo.AlgorithmName);
if (algorithm = 'aes128-cbc') or (algorithm='aes192-cbc') or (algorithm='aes256-cbc') then
algorithm := 'aes'
else
algorithm := '';
if algorithm = '' then
exit;
tmpStream := TMemoryStream.Create;
try
// Calculated password hash
pwdHash := CalcPasswordHash(ADecryptionInfo, APassword);
// Decrypt
iv := DecodeBase64(ADecryptionInfo.InitializationVector);
case algorithm of
'aes':
AErrorMsg := Decrypt_AES_CBC(pwdHash[0], Length(pwdHash)*8, @iv[0], AStream, tmpStream);
else
AErrorMsg := 'Encryption method not supported.';
end;
if (AErrorMsg <> '') then
exit;
// Verify decrypted (but still compressed) stream
// OpenDocument-v1.2-part3, section 3.8.3: "The digest is build from the compressed unencrypted file"
tmpStream.Position := 0;
if not VerifyDecrypt(tmpStream, ADecryptionInfo.EncryptionData_CheckSum, ADecryptionInfo.EncryptionData_ChecksumType) then
AErrorMsg := 'Checksum error';
if AErrorMsg <> '' then
exit;
// Decompress the decrypted stream
Decompress(tmpStream, ADestStream);
ADestStream.Position := 0;
// Success!
Result := true;
finally
tmpStream.Free;
end;
end;
{ Calculates the hash value of the user-provided passwort.
Hash creation is determined by information stored in ADecryptionInfo. }
function TsSpreadOpenDocReaderCrypto.CalcPasswordHash(
ADecryptionInfo: TsOpenDocManifestFileEntry;
APassword: String): TBytes;
var
pwdHash: TBytes;
salt: TBytes;
numIterations: Integer;
keySize: Integer;
begin
Result := nil;
// Generate start key
case LowerCase(ADecryptionInfo.StartKeyGenerationName) of
'sha256': pwdHash := Calc_SHA256(APassword[1], Length(APassword));
else
raise EFpSpreadsheetReader.Create('Unsupported start key generator ' + ADecryptionInfo.StartKeyGenerationName);
end;
// Generate derived key
numIterations := ADecryptionInfo.IterationCount;
keySize := ADecryptionInfo.KeySize;
salt := DecodeBase64(ADecryptionInfo.Salt);
if LowerCase(ADecryptionInfo.KeyDerivationName) = 'pbkdf2' then
Result := PBKDF2_HMAC_SHA1(pwdHash, salt, numIterations, keySize)
else
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
Result := true;
end;
{$IFDEF UNZIP_ABBREVIA}
{ Extracts the specified file from the compressed stream (AStream) to the
ADestStream.
Uses the ABBREVIA library for this purpose (because FCL Stripper fails to
extract the encrypted file). }
function TsSpreadOpenDocReaderCrypto.UnzipToStream(AStream: TStream;
AZippedFile: String; ADestStream: TStream): Boolean;
var
unzipper: TABUnzipper;
begin
Result := false;
unzipper := TABUnzipper.Create(nil);
try
unzipper.Stream := AStream;
try
unzipper.ExtractToStream(AZippedFile, ADestStream);
ADestStream.Position := 0;
Result := true;
except
raise;
end;
finally
unzipper.Free;
end;
end;
{$ENDIF}
{==============================================================================}
initialization
{==============================================================================}
{ Registers this reader for fpSpreadsheet }
sfidOpenDocument_Crypto := RegisterSpreadFormat(sfUser,
TsSpreadOpenDocReaderCrypto, nil,
STR_FILEFORMAT_OPENDOCUMENT, 'ODS', [STR_OPENDOCUMENT_CALC_EXTENSION]
);
end.

View File

@@ -125,22 +125,6 @@ implementation
uses
fpolebasic;
procedure ConcatToByteArray(var OutArray: TBytes; Ptr1: PByte; ACount1: Integer;
Ptr2: PByte; ACount2: Integer);
begin
SetLength(OutArray, ACount1 + ACount2);
if ACount1 > 0 then
Move(Ptr1^, OutArray[0], ACount1);
if ACount2 > 0 then
Move(Ptr2^, OutArray[ACount1], ACount2);
end;
procedure ConcatToByteArray(var OutArray: TBytes; Ptr: PByte; ACount: Integer;
const Arr: TBytes);
begin
ConcatToByteArray(OutArray, Ptr, ACount, @Arr[0], Length(Arr));
end;
function TExcelFileDecryptor.InitEncryptionInfo(AStream: TStream): string;
var
EncInfoStream: TMemoryStream;
@@ -403,7 +387,6 @@ begin
try
inStream := TFileStream.Create(inFileName, fmOpenRead);
inStream.Position := 0;
Result := Decrypt(inStream, outStream, APassword);
finally