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.
Items 5, 6 and 7 apply only to .kdbx files !
The first signature is the same for .kdb and .kdbx files. She says that this file is a KeePass database:
The second signature points to the KeePass version and, therefore, is different for .kdb and .kdbx files:
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 .
After the database is signed, the header begins.
The header of the .kdb file consists of the following fields:
In .kdbx files, each header field consists of 3 parts:
The header of the .kdbx file consists of the following fields:
Master key generation occurs in 2 stages:
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
Password | sha256 (password) |
Key file | sha256 (keyfile) |
Password + Key File | sha256 (concat (sha256 (password), sha256 (keyfile))) |
KeePass 2.x
Password | sha256 (sha256 (password)) |
Key file | sha256 (sha256 (keyfile)) |
Password + Key File | sha256 (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.
<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);
}
Immediately after the header, the encrypted database itself begins. The decryption algorithm is as follows:
<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;
}
Immediately after the End of Header header field, the encrypted database itself begins. The decryption algorithm is as follows:
<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/