Cracking a KeePass database¶
I use KeePassXC as my password manager. I was curious how long it would take to crack my KeePass database if the database file was leaked, and I wanted to learn more about password cracking in general.
Tools¶
I rolled up my sleeves ready to code, only to find out there are existing tools to crack KeePass databases.
The most versatile password cracking tool I found is John the Ripper.
I created a nix shell with a small override to fix a build failure due to a broken symlink.
{
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs = {
self,
nixpkgs,
}: let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
packages.x86_64-linux.john = pkgs.john.overrideAttrs (oA: {
postInstall =
(oA.postInstall or "")
# fix noBrokenSymlinks check
+ ''
rm -rf $out/share/doc
'';
});
devShells.x86_64-linux.default = pkgs.mkShell {
packages = with pkgs; [
keepassxc
self.packages.x86_64-linux.john
];
};
};
}
Basic cracking¶
I created a KeePass database for testing with a really bad password, password12345
.
$ keepassxc-cli db-create --set-password crackme.kdbx
Enter password to encrypt database (optional):
Repeat password:
Successfully created new database.
Using a tool included in John the Ripper I extracted the master password hash from the database file.
keepass2john crackme.kdbx > hash.txt
Inside the hash.txt
file is the password hash:
crackme:$keepass$*2*1000000*0*e5a4354e2df333af6e83378a750563d91c1bd69ecac712f36abe559387f1dd45*a82aa01fa13f7d3602051b6f81872357d25efefcfcf15b245663e68e894e5f75*67da72af0316c85f012a3eda2d23cae7*5ac303ed3451b882c093b75ff9ea225d1a0ed87b980714c84262f475c977db05*7d99ad186d4e7c7824b63ac89f91a1ad1816fe8389516254d6fc210992cdb1af
Using the john
without any configuration took about 5 seconds to crack the database on an AMD Ryzen 9950x CPU.
$ john hash.txt
Warning: detected hash type "KeePass", but the string is also recognized as "KeePass-opencl"
Use the "--format=KeePass-opencl" option to force loading these as that type instead
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass [AES/Argon2 128/128 SSE2])
Cost 1 (t (rounds)) is 1000000 for all loaded hashes
Cost 2 (m) is 0 for all loaded hashes
Cost 3 (p) is 0 for all loaded hashes
Cost 4 (KDF [0=Argon2d 2=Argon2id 3=AES]) is 3 for all loaded hashes
Will run 32 OpenMP threads
Note: Passwords longer than 41 [worst case UTF-8] to 124 [ASCII] rejected
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/nix/store/h1rmrch2pb8cknfcwfyvm2ackn9qy62w-john-rolling-2404/share/john/password.lst
Enabling duplicate candidate password suppressor
password12345 (crackme)
1g 0:00:00:05 DONE 2/3 (2025-02-09 15:14) 0.1757g/s 1968p/s 1968c/s 1968C/s 852741..candace
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
It isn’t surprising that password12345
was fast to crack.
John the Ripper has several cracking modes, and one of the modes is a wordlist.
password12345
is entry number 11212 on the default password list.
$ grep -n password12345 /nix/store/ljkxl6a3wgmfarnxzlswnx2dnk84yysx-john-rolling-2404/share/john/password.lst
11212:password12345
18859:password123456
25909:password123456789
30701:password1234567
32072:password12345678
435049:password1234567890
1170053:password12345678910
GPU acceleration¶
Lets take a look at the warning from earlier.
Warning: detected hash type "KeePass", but the string is also recognized as "KeePass-opencl" Use the "--format=KeePass-opencl" option to force loading these as that type instead
John is telling me I can use OpenCL to offload password cracking to my GPU, which is faster for this purpose.
As usual GPU acceleration didn’t work out of the box.
$ john hash_2word.txt --format=KeePass-opencl
Can't read source kernel: No such file or directory
Luckily there was an open pull-request to nixpkgs to fix this: nixpkgs #353678. After adding this patch to my development shell John was working with OpenCL.
{
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs = {
self,
nixpkgs,
}: let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
packages.x86_64-linux.john = pkgs.john.overrideAttrs (oA: {
postInstall =
(oA.postInstall or "")
# fix noBrokenSymlinks check
+ ''
rm -rf $out/share/doc
''
# fix OpenCL support
+ ''
cp -vrt "$out/share/john" ../run/opencl
'';
});
devShells.x86_64-linux.default = pkgs.mkShell {
packages = with pkgs; [
keepassxc
self.packages.x86_64-linux.john
];
};
};
}
I observed cracking speeds increase between 50% and 500% with my RTX 3090 GPU as compared to my Ryzen 9950x CPU.
Stronger passwords¶
What about a password not on the wordlist?
Lets try two random words separated by an underscore, enforcer_laborious
.
This is also a bad password, but it’s not on the default wordlist.
After heating up my room for 30 minutes I killed the process.
$ john hash_2word.txt --format=KeePass-opencl
Device 1: NVIDIA GeForce RTX 3090
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass-opencl [AES OpenCL])
Cost 1 (t (rounds)) is 1000000 for all loaded hashes
Note: Passwords longer than 41 [worst case UTF-8] to 124 [ASCII] rejected
LWS=32 GWS=335872 (10496 blocks)
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Warning: Only 9857 candidates buffered for the current salt, minimum 335872 needed for performance.
0g 0:00:00:06 DONE 1/3 (2025-02-09 16:43) 0g/s 2924p/s 2924c/s 2924C/s crackme_2word..Crackme2word1900
Proceeding with wordlist:/nix/store/ljkxl6a3wgmfarnxzlswnx2dnk84yysx-john-rolling-2404/share/john/password.lst
Enabling duplicate candidate password suppressor
0g 0:00:28:00 0.15% 2/3 (ETA: 2025-02-22 14:32) 0g/s 3210p/s 3210c/s 3210C/s 2816302..bryce322
Session aborted
Maybe John would be able to crack the database with the default settings given more time.
Lets give John some help. Instead of using a list of known passwords, lets assume the attackers know the format is two words from KeePassXC’s worldlist separated by an underscore. There’s only 7776 words on that list, but using two words from that list results in 77762 = 60,466,176 permutations.
Using a python script I generated a new file with possible candidates.
import itertools
with open("eff_large.wordlist", "r") as f:
words = [line.rstrip() for line in f]
with open("2word.txt", "w") as f:
for word1, word2 in itertools.product(words, repeat=2):
f.write(f"{word1}_{word2}\n")
Then lets give John the new wordlist.
$ john --wordlist=2word.txt --rules=none --format=KeePass-opencl hash_2word.txt
Device 1: NVIDIA GeForce RTX 3090
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass-opencl [AES OpenCL])
Cost 1 (t (rounds)) is 1000000 for all loaded hashes
Note: Passwords longer than 41 [worst case UTF-8] to 124 [ASCII] rejected
LWS=32 GWS=335872 (10496 blocks)
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
Enabling duplicate candidate password suppressor
0g 0:00:18:51 6.13% (ETA: 22:52:46) 0g/s 2969p/s 2969c/s 2969C/s backshift_unsalted..banana_cheek
0g 0:00:30:28 9.92% (ETA: 22:52:07) 0g/s 3123p/s 3123c/s 3123C/s buffoon_endorphin..cacti_lubricant
0g 0:00:46:33 14.87% (ETA: 22:58:12) 0g/s 3126p/s 3126c/s 3126C/s clumsily_antennae..collide_designate
0g 0:00:52:01 16.61% (ETA: 22:58:20) 0g/s 3120p/s 3120c/s 3120C/s congenial_porous..copilot_sprung
0g 0:00:59:15 18.82% (ETA: 22:59:57) 0g/s 3117p/s 3117c/s 3117C/s croak_grant..culinary_paralysis
0g 0:01:04:37 21.02% (ETA: 22:52:28) 0g/s 3205p/s 3205c/s 3205C/s decent_conceal..defiling_freehand
0g 0:01:27:12 27.74% (ETA: 22:59:27) 0g/s 3145p/s 3145c/s 3145C/s economy_limping..elf_regular
enforcer_laborious (crackme_2word)
1g 0:01:32:43 DONE (2025-02-09 19:17) 0.000180g/s 3199p/s 3199c/s 3199C/s enduring_badass..entrap_doubling
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
Even with knowledge of the password format and wordlist it took a 1.5 h to crack.
KeePass database settings¶
keepassxc-cli db-create
creates a database with the following settings:
Format: KDBX 3
Encryption Algorithm: AES 256-bit
Key Derivation Function: AES-KDF (KDBX 3)
Transform Rounds: 1000000
However, KeePass offers more a advanced key derivation function, argon2. Argon2 is a memory-hard algorithm, which means attackers need lots of memory, and therefore lots of money to crack the database.
For my database I use:
Format: KDBX 4
Encryption Algorithm: AES 256-bit
Key Derivation Function: Argon2id
Transform rounds: 4
Memory Usage: 256 MiB
I changed the format of the original database with the password of password12345
and found this format was unsupported by John.
$ keepass2john crackme.kdbx > hash
! crackme.kdbx : File version '40000' is currently not supported!
I rolled up my sleeves ready to code and found that support for KBDX4 and Argon2 had already been added in November 2024: 52294a79d12d5d8aed18454bae3bea6b445950e7.
I updated my nix development shell to use the latest version of John the Ripper.
{
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs = {
self,
nixpkgs,
}: let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
packages.x86_64-linux.john = pkgs.john.overrideAttrs (oA: {
src = pkgs.fetchFromGitHub {
owner = "openwall";
repo = "john";
# bleeding-jumbo tip 2025-02-08
rev = "cc59c0f7074cbb284e164b746f3d805cb54e75b2";
hash = "sha256-wV4fXZo2Pvpe8SBBV3DafFB+7dUde6SwoffzAK35OyI=";
};
postInstall =
(oA.postInstall or "")
# fix noBrokenSymlinks check
+ ''
rm -rf $out/share/doc
''
# fix OpenCL support
+ ''
cp -vrt "$out/share/john" ../run/opencl
'';
});
devShells.x86_64-linux.default = pkgs.mkShell {
packages = with pkgs; [
keepassxc
self.packages.x86_64-linux.john
];
};
};
}
Even though password12345
is on the wordlist the database was much slower to crack after changing to argon2id, taking 2m38s to crack on my RTX 3090 CPU, or 13m13s to crack on my Ryzen 9950x CPU.
$ john hash.txt --format=KeePass-Argon2-opencl
Device 1: NVIDIA GeForce RTX 3090
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass-Argon2-opencl [BlaMka OpenCL])
Cost 1 (t) is 4 for all loaded hashes
Cost 2 (m) is 262144 for all loaded hashes
Cost 3 (p) is 2 for all loaded hashes
Cost 4 (KDF [0=Argon2d 2=Argon2id]) is 2 for all loaded hashes
Note: Passwords longer than 41 [worst case UTF-8] to 124 [ASCII] rejected
Trying to compute 90 hashes at a time using 23040 of 24230 MiB device memory
Trying to compute 45 hashes at a time using 11520 of 24230 MiB device memory
LWS=[32-64] GWS=[2880-2880] ([45-90] blocks) => Mode: WARP_SHUFFLE
Proceeding with single, rules:Single
Press 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status
Almost done: Processing the remaining buffered candidate passwords, if any.
Proceeding with wordlist:/nix/store/ljkxl6a3wgmfarnxzlswnx2dnk84yysx-john-rolling-2404/share/john/password.lst
Enabling duplicate candidate password suppressor
password12345 (?)
1g 0:00:02:38 DONE 2/3 (2025-02-09 15:43) 0.006299g/s 70.58p/s 70.58c/s 70.58C/s selene..19621962
Use the "--show" option to display all of the cracked passwords reliably
Session completed.
During the cracking John used 11.778G of VRAM.
Conclusion¶
I wanted to find out how long it would take to crack my KeePass database. After doing more research I found a better way to word my question is “How much money would it take to crack my KeePass database?”
Based on current GPU prices and cracking speeds a hardened database would take trillions of dollars to crack. Any threat actor with that much money is more likely to take the approach of XKCD 538.
To harden a KeePass database against cracking:
Use a strong password: unique, long, and randomly generated.
Change the default KeePass database settings to argon2.