⬆️ ⬇️

Deciphering the KeePass Database: A Step-by-Step Guide

image




The other day I needed to implement the decryption of the KeePass database. I was struck by the fact that there is not a single document and not a single article with comprehensive information about the decryption algorithm of the .kdb and .kdbx files, taking into account all the nuances. This prompted me to write this article.



At the moment there are 2 versions of KeePass:





The file structure of the KeePass database (.kdb, .kdbx) consists of 3 parts:





Next, I will discuss in detail how to decrypt the database KeePass 1.x and KeePass 2.x.



KeePass Database Decryption



Sequence of actions :


  1. We read the database signature.
  2. We read the database header.
  3. We generate a master key.
  4. Decrypt the database.
  5. Check data integrity.
  6. If the file has been compressed, unpack it.
  7. We decrypt passwords.


Items 5, 6 and 7 apply only to .kdbx files !



Signature


BaseSignature (4 bytes)


The first signature is the same for .kdb and .kdbx files. She says that this file is a KeePass database:





VersionSignature (4 bytes)


The second signature points to the KeePass version and, therefore, is different for .kdb and .kdbx files:





FileVersion (4 bytes)


The third signature is only for .kdbx files and contains the version of the file. For .kdb files, this information is contained in the database header.



Thus, in KeePass 1.x, the signature length is 8 bytes, and in KeePass 2.x - 12 bytes .



Headline


After the database is signed, the header begins.



Heading KeePass 1.x


The header of the .kdb file consists of the following fields:



  1. Flags (4 bytes): This field indicates which types of encryption were used to create the file:
    • 0x01 - SHA256;
    • 0x02 - AES256;
    • 0x04 - ARC4;
    • 0x08 - Twofish.
  2. Version (4 bytes): file version.
  3. Master Seed (16 bytes): used to create a master key.
  4. Encryption IV (16 bytes): used to decrypt data.
  5. Number of Groups (4 bytes): The total number of groups in the database.
  6. Number of Entries (4 bytes): The total number of entries in the database.
  7. Content Hash (32 bytes): hash of decrypted data.
  8. Transform Seed (32 bytes): used to create a master key.
  9. Transform Rounds (4 bytes): used to create a master key.


Heading KeePass 2.x


In .kdbx files, each header field consists of 3 parts:



  1. Field ID (1 byte): possible values ​​from 0 to 10.
  2. Data length (2 bytes).
  3. Data ([data length] bytes)


The header of the .kdbx file consists of the following fields:





Master key generation


Master key generation occurs in 2 stages:



  1. Generate a composite key;
  2. Generate master key based on composite key.


1. Generate a composite key


To generate the composite key, the SHA256 hash algorithm is used. The tables below show the pseudo-code for generating the composite key, based on which version of KeePass is used, and what input data is needed to decrypt the database (only the password, only the key file or all together):



KeePass 1.x



Passwordsha256 (password)
Key filesha256 (keyfile)
Password + Key Filesha256 (concat (sha256 (password), sha256 (keyfile)))


KeePass 2.x



Passwordsha256 (sha256 (password))
Key filesha256 (sha256 (keyfile))
Password + Key Filesha256 (concat (sha256 (password), sha256 (keyfile)))
Windows User Account (WUA)sha256 (sha256 (WUA))
Password + Key + File (WUA)sha256 (concat (sha256 (password), sha256 (keyfile), sha256 (WUA)))


I draw attention to the fact that if several entities are needed to decrypt a database (for example, a password and a key file), you first need to get a hash from each entity, and then connect them together (concat) and take a hash from the combined sequence.



2. Generate master key based on composite key


  1. You need to encrypt the composite key obtained above using the AES-256-ECB algorithm.
    • As a key you need to use Transform Seed from the header.
    • This encryption needs to be performed Transform Rounds (from the header) times.
  2. Using SHA256, we get a hash from an encrypted composite key.
  3. We connect Master Seed from the header with the received hash.
  4. With SHA256, we get a hash from the combined sequence - this is our master key!


Pseudocode
<p>void GenerateMasterKey() { //   TransformRounds  for(int i = 0; i < TransformRounds; i++) { result = encrypt_AES_ECB(TransformSeed, composite_key); composite_key = result; }</p> <source>//      hash = sha256(composite_key); //     MasterSeed   key = concat(MasterSeed, hash); //      master_key = sha256(key); 


}



Decryption of KeePass 1.x data


Immediately after the header, the encrypted database itself begins. The decryption algorithm is as follows:



  1. The entire remaining piece of the file is decrypted using the AES-256-CBC algorithm.
    • As a key, use the master key generated above.
    • As initialization vector we use Encryption IV from the header.
  2. The last few bytes of the decrypted database are redundant - they are several identical bytes at the end of the file (padding). To eliminate their influence, you need to read the last byte of the decrypted database - this is the number of "extra" bytes, which should not be further taken into account.
  3. Using SHA256, we get the hash from the decrypted data (the bytes from the previous paragraph are not taken into account ).
  4. Check that the resulting hash matches the Content Hash field from the header:
    • If the hash matches, we have successfully decrypted our database! You can save the decrypted data as an .xml file and make sure that all logins with passwords are decoded correctly,
    • If the hash does not match, it means that either a wrong password or key file was provided, or the data was corrupted.


Pseudocode
 <p>bool DecryptKeePass1x() { //    //(  -   -  ) db_len = file_size - signature_size - header_size;</p> <source>//  decrypted_data = decrypt_AES_256_CBC(master_key, EncryptionIV, encrypted_data); //  ""  extra = decrypted_data[db_len - 1]; //    (  extra !) content_hash = sha256(decrypted_data[:(db_len - extra)]); //,       ontentHash   if (ontentHash == content_hash) return true; else return false; 


}



Decryption of KeePass 2.x data


Immediately after the End of Header header field, the encrypted database itself begins. The decryption algorithm is as follows:



  1. The entire remaining piece of the file is decrypted using the AES-256-CBC algorithm.
    • As a key, use the master key generated above.
    • As initialization vector we use Encryption IV from the header.
  2. The last few bytes of the decrypted database are redundant - they are several identical bytes at the end of the file (padding). To eliminate their influence, you need to read the last byte of the decrypted database - this is the number of "extra" bytes, which should not be further taken into account.
  3. Check that the first 32 bytes of the decrypted database coincide with the Stream Start Bytes field of the header:
    • If the data matches, then we have generated the correct master key,
    • If the data does not match, it means that either an incorrect password was provided, a key file or WUA, or the data was corrupted.
  4. If the previous item is completed successfully, discard the first 32 bytes. Check the Compression Flags field of the header. If GZip file compression was used, then unpack the data.
  5. Getting to check the integrity of the data. The data is divided into blocks, the maximum block size is 1024 * 1024. Each data block begins with a header. The header structure is as follows:
    • Block ID (4 bytes): block number starting from 0;
    • Block data hash (32 bytes);
    • Block size (4 bytes).
  6. Therefore, the procedure is as follows:
    • Read the block header.
    • Read block data.
    • Using SHA256, we get a hash from the block data.
    • We check that the hash matches the hash from the header.
  7. We carry out the sequence of actions from the previous paragraph for each data block. If the data in all blocks is saved, then we cut out all the block headers, and the resulting sequence is the decrypted database.
  8. ATTENTION : even in the decrypted .kdbx file passwords can be in encrypted form.
  9. We save the decrypted and decapitated data as an .xml file.
  10. We find in it all the nodes with the name "Value", the attribute "Protected", the value of this attribute "True" and take the values ​​of these nodes. These are still encrypted passwords.
  11. We decode all encrypted passwords using the base64decode algorithm.
  12. In the Inner Random Stream ID header field, we look at which algorithm was used to encrypt passwords. In my case it was Salsa20.
  13. We generate a pseudo-random 64-byte sequence using the Salsa20 algorithm:
    • As a key, we use the hash of the Protected Stream Key header field, obtained using SHA256.
    • As the initialization vector we use the constant 8-byte sequence 0xE830094B97205D2A.
  14. IMPORTANT: With this 64-byte sequence, you can decrypt exactly 64 characters in order of the combined decoded passwords . If this is not enough to decrypt all passwords, you need to generate the following pseudo-random sequence and continue to decrypt passwords, etc. to end.
  15. To obtain the final password, it is necessary to make an XOR decoded using base64decode password with a pseudo-random sequence obtained in the previous paragraph (more clearly, the sequence of actions is presented in the pseudo-code below).
  16. VERY IMPORTANT : Passwords must be decrypted in order! It is in the sequence in which they are presented in the xml file.
  17. We find in the xml file all the nodes with the name "Value", the attribute "Protected", the value of this attribute "True":
    • Replace attribute value with “False”.
    • The value of the node is replaced by the decoded password.
  18. And only now we have received the fully decrypted KeePass 2.x database! Hooray! =)


Pseudocode
 <p>bool DecryptKeePass2x() { //    //(  -   -  ) db_len = file_size - signature_size - header_size;</p> <source>//  decrypted_data = decrypt_AES_256_CBC(master_key, EncryptionIV, encrypted_data); //  ""  extra = decrypted_data[db_len - 1]; db_len -= extra; //,   32    //   StreamStartBytes  if (StreamStartBytes != decrypted_data[0:32]) return false; //  32  db_len -= 32; decrypted_data += 32; //  CompressionFlag  //   ,   if (CompressionFlag == 1) unzip(decrypted_data); //   while (db_len > (BlockHeaderSize)) { //    block_data = decrypted_data[0:BlockHeaderSize]; decrypted_data += BlockHeaderSize; db_len -= BlockHeaderSize; if (block_data.blockDataSize == 0) { break; } //    hash = sha256(decrypted_data[0:block_data.blockDataSize]); //,         if(block_data.blockDataHash == hash) { pure_data += decrypted_data[0:block_data.blockDataSize]; decrypted_data += block_data.blockDataSize; db_len -= block_data.blockDataSize; } else { return false; } } //      xml  xml = pure_data.ToXml(); //    ProtectedStreamKey  key = sha256(ProtectedStreamKey); //  Salsa20 IV_SALSA = 0xE830094B97205D2A; salsa.setKey(key); salsa.setIv(IV_SALSA); stream_pointer = 0; key_stream[64] = salsa.generateKeyStream(); //  while(true) { //      "Value", // "Protected",   "True" node = xml.FindNextElement("Value", "Protected", "True"); if (node == NULL) { break; } //        base64decode decoded_pass = base64decode(node.value); //      key_stream for (int i = 0; i < len(decoded_pass); i++) { decoded_pass[i] = decoded_pass[i] ^ key_stream[stream_pointer]; stream_pointer++; // 64     , //    if (stream_pointer >= 64) { key_stream[64] = salsa.generateKeyStream(); stream_pointer = 0; } } //   "Protected"  "False" node.attribute.value = "False"; //    node.value = decoded_pass; } return true; 


}



That's basically all that I wanted to tell. I hope this manual will save someone from an extra headache and will be informative and informative =)



')

Source: https://habr.com/ru/post/346820/



All Articles