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, fpsUtils, 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.