One can certainly argue about the security of Keepass. I for one am currently using it to store my passwords, mainly because I do not necessarily trust any web service to handle my passwords. At least with Keepass I can make sure that my file only goes to places where I think it should go.
(Un)Fortunately I have a couple of different devices running diferent operating systems. I have a Macbook at work, a Windows PC at home and a Linux notebook for my private coding projects.
If you know Keepass 2, you know that it is written in C# using dotNet. Running it on an OS not called Windows is somewhat annoying. You can get it to work using Mono on OS X but I had lots of issues with that approach (it kept crashing...).
In order to still be able to access my Keepass file, I am using keepass-node. It works well for reading files. Saving files is another issue as it broke my file making it unreadable for the Windows version of Keepass 2. It is far from perfect, but it is sufficient.
The thing that annoys me about keepass-node though is, that I always have to open it up in a browser and to get it running initially a lot of stuff has to be installed. It would be great if it only required a single binary, CLI would be fine for me.
With that in mind I figured: why not build it in Go?
I started to look around trying to find some proper specification of the file format but either I did not look hard enough or there just isn't any. Thus: use the source! - so I did.
I looked at keepass.io, the library used by keepas-node, as I knew that it works and I was not eager to look at the C# source on sourceforge.
An encrypted keepass file consists of 3 parts:
- A signature part with general information about the file
- A header part containing all kinds of information necessary for decrypting the file
- The actual (encrypted) content
The signature consists of 12 bytes, 4 bytes each for a BaseSignature, a VersionSignature and a FileVersion (in that order). The BaseSignature should match
0x03, 0xd9, 0xa2, 0x9a and the VersionSignature should match
0x67, 0xfb, 0x4b, 0xb5.
There are 10 (possible) headers in a Keepass file:
Each header consists of 1 byte containing the ID (same value as in the list above), 2 bytes
little endian uint16 containing the length of the header content and then the number of bytes as just given for the actual content.
Mostly this content can just be taken directly as binary.
CompressionFlags was parsed as
little endian uint32 in keepass.io so I figured I would do the same - interestingly I did not have to use that value though.
TransformRounds header consists of 8 bytes. You can parse it as a
little endian uint64.
The data itself is
AES-256-CBC encrypted. For decryption a key and an IV are required. The IV can be found amongst the headers labeled
EncryptionIV. The key itself has to be derived first.
The windows version of Keepass 2 provides 3 ways to secure your file: a password, a key file and the Windows user account. For portability the Windows user account is not all that useful, thus it will be ignored.
The key file is either directly regarded as binary key or as an XML file containing
<data></data> which encapsulates the key.
This content or the password provided will be hashed twice with SHA-256.
Then the two 16 byte blocks of the key have to be transformed.
The number of
AES-256-ECB on each block.
Since Go does not directly support ECB (meaning there is no function simply providing it) a bit of background knowledge can help.
ECB uses the same block cipher that
CBC uses with the slight difference, that
CBC requires an initialization vector and passes the result of a block as vector into the next block. You can use an empty initialization vector and only encrypting a single block though to emulate the behaviour of
ECB. Note that
ECB is NOT suited for encrypting larger amounts of data as any block equivalent to another block will also have the same result after encryption making it easy to attack. Trying to read a given format though there is no choice but to use it.
The key derivation works by using the
TransformSeed as the encryption key, an empty (all zeros) IV and the individual 16 byte blocks of the hash as plaintext.
The result of these transformation rounds will be appended to the
MasterSeed and hashed again using
SHA-256. The result is the key which is required to decrypt the data.
As already mentioned the content is AES encrypted using 256 byte
CBC thus simply decrypting it using our derived key and the
The resulting content contains bytes for IDs and hashes to allow integrity checks (MAC-then-encrypt).
StreamStartBytes can be used to compare the same amount of bytes from the decrypted data to ensure proper decrypt.
The first 4 bytes of the decrypted data are an index for the block number (so theoretically the order of the content could be random and should be ordered now - fortunately it worked ignoring this). The next 32 bytes are a hash of the content. The length of the content in byte is defined by the next 4 bytes as
little endian uint32, followed by the content.
Until the end of the file there can be multiple such blocks.
SHA-256 of a content unit should match the 32 bytes hash value. If all hashes match the data can be appended to each other.
The result of this is a gzip-ed block of bytes. Simply unzipping it is sufficient.
The unzipped data now is simple XML and can be parsed as such. Fortunately the XML encoding package in go is working just as well as the JSON encoding package to unmarshal data into a struct.
An example of the decrypted XML can be found here.
Unfortunately the passwords are still protected using Salsa20 as a stream cipher - with a custom implementation. It works by unlocking ALL protected password entries in the correct order. Basically this order is: Top to bottom in the file (depth first search) on anything that has an entry with a password. This includes entries for the password history.
In hindsight I have to say that go was a great choice to do this. I am doing a lot of Ruby at work which probably would not have been as nice to do this. Also I would be back at having dependencies (Ruby) to be instaled before being able to use it.
Furthermore it was great that everything can be (easily) done using the standard library. It took me a while to figure out how to do
ECB though as I am not that proficient in anything related to crypto.
I released the code for it on Github. Check it out and open an issue if you notice any major mistakes I made in handling crypto or send me a pull request.
For now it is a rather simple port of keepass.io and does not take clearing memory into account.
I reached my goal (being able to read a keepass file) with this and thus I am not sure how much work I will put in in the near future. Especially since I am exploring alternatives for Keepass.
In an earlier post I wrote about how the golang challenge is good for learning new things around go in a small task. My participation in the first challenge did help me with this.
In case you are wondering why not all different headers were mentioned in usage in this post: Not all of them are actually necessary as there is only 1 option.
CipherID for example does not do anything at the moment as there is only one option.