Help with decrypting OpenDocument files lazarus / 49-FPSpreadsheet ---------------------------------------------------------------------- wp · 2023-07-26 16:44 Motivated by a recent post whether FPSpreadsheet can read pass-word protected ods files I began to investigate how decryption of such files could be achieved. Found this nice article: https://ringlord.com/dl/Decrypting%20ODF%20Files.pdf, slowly got the idea and began coding... The attached project tries to decrypt the file "content.xml" extracted from an ods file which had been encrypted with the password "123". All the required decryption parameters such as salt, initialization vector, algorithms used, were extracted from the META-INF/manifest.xml file contained in the ods file. But it does not work - I cannot restore the original file content, something is wrong... Since I myself am quite inexperienced with encryption/decryption I am asking: Is there anybody out there who could give me a hint on what might be wrong, or who could even fix the test application? P.S. The attached project depends on the DCPCrypt package (available in OPM, installation not required). The original, encrypted ods file is contained in the archive. ---------------------------------------------------------------------- rvk · 2023-07-26 17:19 I can remember I looked into this. I'm not sure how far I got. I will try to see what you missed later on this evening (if I get some time). Quote from: rvk on January 17, 2022, 06:05:59 pmYou would need to unzip the content of the ods yourself (not protected) and look in the META-INF/manifest.xml. It will show how the files are encrypted (probably aes256-cbc). The initialisation-vector (IV) is also in there. With that you could try to decrypt all the files mentioned in that manifest.xml  (a recent post here shows some code to decrypt AES-256-CBC with TDCP_rijndael). After that you can rezip the contents back to an unprotected ods-file and read it with fpspreadsheet. (Don't forget to remove the encryption-data parts in the manifest.xml) Maybe you also need to do something (PBKDF2) with the password. See https://askubuntu.com/a/223183/926892 for more details. Quote from: rvk on January 07, 2022, 11:16:36 pmAt the moment this problem is solved. https://www.nldelphi.com/showthread.php?43955-Decrypt-string-from-PHP- met-DCPCrypt&p=367627&viewfull=1#post367627 Because openssl_encrypt() in PHP was used without OPENSSL_RAW_DATA as option, openssl_encrypt() gives the result as a base64 encoded string. When you in turn want to use that data in DecryptCBC() you need to first base64decode it. So, together with padding the decoded key back to 32 characters, this was sufficient to make it work. Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i DataString := DecodeStringBase64(datarr[0]); // nu is de lengte wel goed Complete function: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i function my_decrypt(Data: string; key: string): string;var  datarr: TArrayOfString;  Cipher: TDCP_rijndael;  DataString, encryption_key, IV: ansistring;begin  datarr := SplitString('::', DecodeStringBase64(Data), 2);  encryption_key := DecodeStringBase64(key);  if (Length(encryption_key) < 32) then    while Length(encryption_key) < 32 do encryption_key := encryption_key + ansichar(0);  encryption_key := Copy(encryption_key, 1, 32);  IV := datarr[1];  DataString := DecodeStringBase64(datarr[0]); // nu is de lengte wel goed  Cipher := TDCP_rijndael.Create(nil);  Cipher.CipherMode := cmCBC;  Cipher.Init(encryption_key[1], Length(encryption_key) * 8, @IV[1]);  Cipher.DecryptCBC(DataString[1], DataString[1], Length(DataString));  my_decrypt := DataString;  Cipher.Free;end; ---------------------------------------------------------------------- Thaddy · 2023-07-26 20:16 Base64 is NOT a crypt. That is just about efficient storage. I suspect the necessary step to decode the SHA is left out. ---------------------------------------------------------------------- rvk · 2023-07-26 20:18 Quote from: wp on July 26, 2023, 06:44:33 pmSince I myself am quite inexperienced with encryption/decryption I am asking: Is there anybody out there who could give me a hint on what might be wrong, or who could even fix the test application? I don't see very much wrong with your method. But... didn't you forget point 2 from the encryption stage? So when doing stuff in reverse you should deflate (decompress) the result before showing it. (I tried to implement it but didn't have much success.) Quote2. Data Compression and Encryption a) Each sensitive document component is deflated (compressed) with the algorithm implemented by zlib (and used by gzip and similar zip tools); header and footer components part of the gzip file format are not stored4, b) Each such deflated component is then encrypted using a random initialization vector to seed the engine, followed by the cryptographic key from step 1 above, using either Bruce Schneier's 128-bit Blowfish cipher in CFB (Cyclic Feedback) mode, or the United States government's 256-bit AES (Advanced Encryption Standard) in CBC (Cipher-block chaining) mode. c) The name of the key strengthening algorithm and cryptographic cipher, as well as the value of the salt, initialization vector, and the SHA1/1K or SHA256/1K (SHA1/SHA256 of the first 1024 bytes of encrypted data) is stored into the META-INF/manifest.xml file for the sensitive document component. The code listed in this document reverses these steps. BTW. The result of DataString is different each time. I can't imagine that's correct. Quote from: Thaddy on July 26, 2023, 10:16:33 pmBase64 is NOT a crypt. That is just about efficient storage. I suspect the necessary step to decode the SHA is left out. Yeah, reading only half the post is your trademark. SHA was done. The topic I mentioned was about something else but I added it for the TDCP_rijndael with key and IV. wp already implemented that, almost exactly, as I had it in the other topic. ---------------------------------------------------------------------- wp · 2023-07-26 20:35 Quote from: rvk on July 26, 2023, 10:18:47 pmBut... didn't you forget point 2 from the encryption stage? So when doing stuff in reverse you should deflate (decompress) the result before showing it. Yes I read that, and I did not understand it. Because in order to get the xml file I already must have extracted it from the ods which is a zip. Or do you want to say that an encrypted xml file is compressed twice: once as one step of the encryption sequence, and once again when all files are compressed into a single zip? ---------------------------------------------------------------------- rvk · 2023-07-26 20:39 Quote from: wp on July 26, 2023, 10:35:49 pmQuote from: rvk on July 26, 2023, 10:18:47 pmBut... didn't you forget point 2 from the encryption stage? So when doing stuff in reverse you should deflate (decompress) the result before showing it. Yes I read that, and I did not understand it. Because in order to get the xml file I already must have extracted it from the ods which is a zip. Or do you want to say that an encrypted xml file is compressed twice: once as one step of the encryption sequence, and once again when all files are compressed into a single zip? Yes, as I see it there, it says "Each sensitive document component is deflated (compressed)". And context.xml is such a component. So it should be deflated as a last step. But... I don't thing the DataString should be different each time. Running it through TDCP_rijndael it keeps giving me a different DataString (printed as base64encoded for display-compare). I put a loop of 3 times around the whole code (including the .free's) and it gives different result. As long as it does that, the result would be wrong anyway, so trying to inflate (decompress) that wouldn't even make sense. ---------------------------------------------------------------------- wp · 2023-07-26 21:43 Quote from: rvk on July 26, 2023, 10:39:49 pmQuote from: wp on July 26, 2023, 10:35:49 pmQuote from: rvk on July 26, 2023, 10:18:47 pmBut... didn't you forget point 2 from the encryption stage? So when doing stuff in reverse you should deflate (decompress) the result before showing it. Yes I read that, and I did not understand it. Because in order to get the xml file I already must have extracted it from the ods which is a zip. Or do you want to say that an encrypted xml file is compressed twice: once as one step of the encryption sequence, and once again when all files are compressed into a single zip? Yes, as I see it there, it says "Each sensitive document component is deflated (compressed)". And context.xml is such a component. So it should be deflated as a last step. OK, reading it this way it even makes sense because encrypting a noise-like compressed input stream certainly makes it more diffcult for crackers to find regular patterns in the encrypted data for hacking the password. ---------------------------------------------------------------------- rvk · 2023-07-26 22:42 I now got consistent decryption with the following: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i cipher.Init(key[1], Length(key) * 8, @iv[1]);(note the key[1]    ) Now it's bedtime ---------------------------------------------------------------------- wp · 2023-07-27 09:29 Quote from: rvk on July 27, 2023, 12:42:26 am(note the key[1]) Oh - me stupid... But one solved problem leads to the next one... I now added a decompression step after the decryption: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i uses   ZStream;procedure Decompress(SrcStream, DestStream: TStream);var  decompressor: TDecompressionStream;begin  decompressor := TDecompressionStream.Create(SrcStream, true);  // false);  // ????  try    DestStream.Size := 0;    DestStream.CopyFrom(decompressor, 0);  // 0 --> entire src stream  finally    decompressor.Free;  end;end; But when the decrypted stream is fed into this procedure, there is a "data error". Maybe because "header and footer components part of the gzip file format are not stored" (see reply #3). The second parameter of the TDecompressionStream construction is named "ASkipHeader"; I tried both "true" and "false" settings - always the same error. See attached project "pwd123_(unzipped)-v2" And maybe this is related to the issue that I cannot extract the encrypted xml files from the ods by means of our zipper unit. The "content.xml" of the previous example was extracted by using an external application (PowerArchiver), but when I try to do the same with zipper I only get files of zero size - see attached project "unzip_test" ---------------------------------------------------------------------- rvk · 2023-07-27 09:44 Last time I tried yesterday I switched to both TMemoryStream and did away with the rawbytestring. I'm not sure how FPC handles #0 inside a rawbyte string (if it cuts of the string it all falls apart). With TMemoryStream the deflate also didn't work but I got back all 8K268K268K268... (only 258 bytes in length). No error. Quote from: wp on July 27, 2023, 11:29:23 amBut when the decrypted stream is fed into this procedure, there is a "data error". Maybe because "header and footer components part of the gzip file format are not stored" (see reply #3). The second parameter of the TDecompressionStream construction is named "ASkipHeader"; I tried both "true" and "false" settings - always the same error. See attached project "pwd123_(unzipped)-v2"It should definitely be true (SkipHeader) because there is no header and no footer in the file. Quote from: wp on July 27, 2023, 11:29:23 amAnd maybe this is related to the issue that I cannot extract the encrypted xml files from the ods by means of our zipper unit. The "content.xml" of the previous example was extracted by using an external application (PowerArchiver), but when I try to do the same with zipper I only get files of zero size - see attached project "unzip_test" Encrypted files is STORED when put in the final ods/zip. This is logical because the are already compressed before going through the encryption phase. And that's also needed to be able to check the size of that file in the header of the zip. It's explained in that PDF. I'm not sure if Zipper supports STORED files inside a zip. ---------------------------------------------------------------------- rvk · 2023-07-27 16:45 New day, new chances. I can confirm the generated key is correct in FPC. I checked with this on Linux and it gave the same base64 encoded key (for display purposes I used the base64). (that BuRJumZa52y4SXQkP98QduGoLFT2+xykDFJyayzQZx4= is the same one) Code: Bash  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i $ php << 'EOF'EOFkey hex: 06e449ba665ae76cb84974243fdf1076e1a82c54f6fb1ca40c52726b2cd0671ekey b64: BuRJumZa52y4SXQkP98QduGoLFT2+xykDFJyayzQZx4=$ I also checked with the following if the decryption worked correctly. And here too with base64 I got the same result as in FPC Code: Bash  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i $ openssl enc -d -aes-256-cbc -nopad -nosalt -in content.xml -K 06e449ba665ae76cb84974243fdf1076e1a82c54f6fb1ca40c52726b2cd0671e -iv 2c3d4e79b6afabf0d2eb0bdd1ee3a2d2 -p -out c.xml.gzkey=06E449BA665AE76CB 84974243FDF1076E1A82C54F6FB1CA40C52726B2CD0671Eiv =2C3D4E79B6AFABF0D2EB0BDD1EE3A2D2$ cat c.xml.gz | base64         # is the same as in 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$ But the resulting c.xml.gz doesn't seem to be inflatable. Even when adding header and footer for 'proper' .gz file. (I read somewhere that gunzip should unzip it even if the header and footer didn't match, but that doesn't seem to be the case) Code: Bash  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i $ echo -ne '\x1f\x8b\x08\x00\x00\x00\x00\x00\00\03' > content.xml.gzcat c.xml.gz >> content.xml.gzecho -ne '\x00\x00\x00\x00\x00\x00\x00\x00' >> content.xml.gz$ cat content.xml.gz | gunzip -c      # error gzip: stdin: invalid compressed data--crc error gzip: stdin: invalid compressed data--length error$ So now we need a confirmed working way to inflate without header (without resorting to the java.util.zip.Inflater class  ). ---------------------------------------------------------------------- rvk · 2023-07-27 16:56 I GOT IT. (I'll test it some more but it seems I figured it out  ) Be back soon. ---------------------------------------------------------------------- rvk · 2023-07-27 17:11 Ok... here it goes. I went back to your last original example with the streams. I changed the srcStream to a TStringStream (same as destStream) to get to the DataString easily. Although I don't like using strings for this (even raw strings) it does seem to work. The first problem was that key[1] in Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i cipher.Init(key[1], Length(key)*8, @iv[1]); And now the kicker... The PBKDF2() needs to be done with SHA1. Not SHA256 !!! Although the file says xmldsig#sha256 this is only for the FIRST INITIAL digest. NOT for the following PBKDF2. So: Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i key := PBKDF2(pwdHash, salt, keyDerivationCount, keyDerivationKeySize, TDCP_SHA1); Now adding the inflating of the stream and it all works. In my code below I reused srcStream because it was already there. The code could use a lot of cleanup and some things could be done more efficient... but it shows that it works !!! BTW, there is a checksum with which you can check the extracted data is correct. There are also multiple methods for checksum and encryption (which the code should probably all handle  ) Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i uses ..., zstream;//..procedure Decompress(SrcStream, DestStream: TStream);var  decompressor: TDecompressionStream;begin  decompressor := TDecompressionStream.Create(SrcStream, true);  // false);  // ????  try    DestStream.Size := 0;    DestStream.CopyFrom(decompressor, 0);  // 0 --> entire src stream  finally    decompressor.Free;  end;end; // ... var  pwdHash: RawByteString;  key: RawByteString;  salt: RawByteString;  iv: RawByteString;  cipher: TDCP_blockcipher;  srcStream: TStringStream;  deststream: TStringStream;  n: integer;begin  // Calculate password hash  pwdHash := CalcDigest(pwd, TDCP_sha256);   // Calculated key as strengthened password  salt := Base64DecodeStr(keyDerivationSalt);  key := PBKDF2(pwdHash, salt, keyDerivationCount, keyDerivationKeySize, TDCP_SHA1);   // Decrypt  iv := Base64DecodeStr(cipherInitializationVector);  cipher := TDCP_rijndael.Create(nil);  try    cipher.Init(key[1], Length(key)*8, @iv[1]);    cipher.CipherMode := cmCBC;    srcStream := TStringStream.Create;    destStream := TStringStream.Create;    try    srcStream.LoadFromFile('content.xml');      n := cipher.DecryptStream(srcStream, destStream, srcStream.Size);       writeln('-------------------------------');      destStream.Position := 0;      srcStream.Clear;      Decompress(destStream, srcStream);    WriteLn(srcStream.DataString);      writeln('-------------------------------');     finally      destStream.Free;      srcStream.Free;    end;  finally    cipher.Free;  end;   WriteLn();  Write('Press ENTER to close...');  ReadLn;end. Above code shows this: Note the extra characters at the top. I don't know what they are. They are not in your decrypted file but perhaps that's because you just took a unencrypted file. Quote------------------------------- 132639 ------------------------------- Press ENTER to close... ---------------------------------------------------------------------- wp · 2023-07-27 21:05 Hey amazing! Excellent job! QuoteAlthough the file says xmldsig#sha256 this is only for the FIRST INITIAL digest.Well - this part belongs to the "manifest:start-key- generation", and this refers to the first initial digest indeed. But if I'd have looked at the Schuermann's pdf more carefully I would have seen the SHA1 in this code snippet line: Code: Java  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i byte[] dk = PBKDF2.deriveKey( pwdHash, salt, iterationCount, 16 );Key key = new SecretKeySpec( dk, “PBKDF2WithHmacSHA1” ); QuoteNote the extra characters at the top.Yes this had been added now. Well - at least it still looks like valid xml, and the fpspreadsheet reader will not be affected by it. Now I need to investigate why zipper extracts encrypted files with size 0 at least to the extent that I can write a bug report. ---------------------------------------------------------------------- rvk · 2023-07-27 21:19 Quote from: wp on July 27, 2023, 11:05:34 pmBut if I'd have looked at the Schuermann's pdf more carefully I would have seen the SHA1 in this code snippet line: Code: Java  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i byte[] dk = PBKDF2.deriveKey( pwdHash, salt, iterationCount, 16 );Key key = new SecretKeySpec( dk, “PBKDF2WithHmacSHA1” );Indeed. But because there was mention there that SHA1 could also be possible (also for the initial digest) I also overlooked it there completely  Quote from: wp on July 27, 2023, 11:05:34 pmQuoteNote the extra characters at the top.Yes this had been added now. Well - at least it still looks like valid xml, and the fpspreadsheet reader will not be affected by it.Yes, it's between <-- --> so it's just comment. Probably some extra checksum or something. (there is probably some explanation in the latest documentation but I haven't looked because it's not needed) Quote from: wp on July 27, 2023, 11:05:34 pmNow I need to investigate why zipper extracts encrypted files with size 0 at least to the extent that I can write a bug report. Yeah. STORED can happen (especially if you zip already highly compressed files). So zipper should be able to unzip them. If I zip files with 7zip and use 0 - Store as compression level, I notice the resulting zip has "Copy" as method. While the pwd 123.ods (when opened with 7zip) shows "Store" as method. Maybe zipper can handle "Copy" but not "Store"  (you could create a zip with 7zip ---------------------------------------------------------------------- rvk · 2023-07-28 09:24 Quote from: rvk on July 27, 2023, 11:19:20 pmIf I zip files with 7zip and use 0 - Store as compression level, I notice the resulting zip has "Copy" as method. While the pwd 123.ods (when opened with 7zip) shows "Store" as method. Maybe zipper can handle "Copy" but not "Store"  (you could create a zip with 7zip Sorry about this confusion but the "Copy" method in 7zip is only for 7zip format, not for .zip  I did notice that the file mimetype in "no pwd.zip" is also Store, and that one can be extracted. It can even be extracted from the "pwd 123.zip". Weird that content.xml fails. I also noticed that the "Version" in the pwd and nopwd are 20, while my 7zip created zip with version 10. But because zipper can extract the content.xml from no pwd (with version 20) this should matter much. ---------------------------------------------------------------------- wp · 2023-07-28 21:51 Used the code below to unzip the "pwd 123.ods" file in Delphi, and this extracts the encrypted files correctly. Indicates that there really is a bug in our zipper... Code: Pascal  [Select][+][-]window.onload = function(){var x1 = document.getElementById("main_content_section"); if (x1) { var x = document.getElementsByClassName("geshi");for (var i = 0; i // Delphi code!uses  System.SysUtils,  System.Zip;var  zipfile: TZipFile;begin  zipFile := TZipFile.Create;  try    zipFile.Open('..\..\..\pwd 123.ods', zmRead);    zipFile.Extract('content.xml');    zipFile.Close;  finally    zipFile.Free;  end;end. ---------------------------------------------------------------------- wp · 2023-07-29 14:45 Submitted a bug report to FPC: https://gitlab.com/freepascal.org/fpc/source/-/issues/40369