#!/usr/bin/env perl ## ## Author......: See docs/credits.txt ## License.....: MIT ## use strict; use warnings; use Crypt::CBC; use Crypt::Mode::ECB; use Digest::SHA qw (sha256); sub module_constraints { [[0, 256], [-1, -1], [-1, -1], [-1, -1], [-1, -1]] } sub get_random_keepass_salt { my $version = random_number (1, 2); my $algorithm; my $iteration; my $final_random_seed; if ($version == 1) { $algorithm = random_number (0, 1); $iteration = random_number (50000, 99999); $final_random_seed = random_bytes (16); $final_random_seed = unpack ("H*", $final_random_seed); } elsif ($version == 2) { $algorithm = 0; $iteration = random_number (6000, 99999); $final_random_seed = random_bytes (32); $final_random_seed = unpack ("H*", $final_random_seed); } my $transf_random_seed = random_bytes (32); $transf_random_seed = unpack ("H*", $transf_random_seed); my $enc_iv = random_bytes (16); $enc_iv = unpack ("H*", $enc_iv); my $contents_hash = random_bytes (32); $contents_hash = unpack ("H*", $contents_hash); my $inline_flag = 1; my $contents_len = random_number (128, 499); my $contents = random_bytes ($contents_len); $contents_len += 16 - $contents_len % 16; $contents = unpack ("H*", $contents); my $salt_buf; my $is_keyfile = random_number (0, 1); my $keyfile_attributes = ""; if ($is_keyfile == 1) { $keyfile_attributes = $keyfile_attributes . "1*64*" . unpack ("H*", random_bytes (32)); } if ($version == 1) { $salt_buf = $version . '*' . $iteration . '*' . $algorithm . '*' . $final_random_seed . '*' . $transf_random_seed . '*' . $enc_iv . '*' . $contents_hash . '*' . $inline_flag . '*' . $contents_len . '*' . $contents . '*' . $keyfile_attributes; } elsif ($version == 2) { $contents = random_bytes (32); $contents = unpack ("H*", $contents); $salt_buf = $version . '*' . $iteration . '*' . $algorithm . '*' . $final_random_seed . '*' . $transf_random_seed . '*' . $enc_iv . '*' . $contents_hash . '*' . $contents . '*' . $keyfile_attributes; } return $salt_buf; } sub module_generate_hash { my $word = shift; my $salt = shift; my $param = shift; if (length $salt == 0) { $salt = get_random_keepass_salt (); } my @salt_arr = split ('\*', $salt); my $version = $salt_arr[0]; my $iteration = $salt_arr[1]; my $algorithm = $salt_arr[2]; my $final_random_seed = $salt_arr[3]; my $transf_random_seed = $salt_arr[4]; my $enc_iv = $salt_arr[5]; my $contents_hash; # specific to version 1 my $inline_flag; my $contents_len; my $contents; # specific to version 2 my $expected_bytes; # specific to keyfile handling my $inline_keyfile_flag; my $keyfile_len; my $keyfile_content; my $keyfile_attributes = ""; $final_random_seed = pack ("H*", $final_random_seed); $transf_random_seed = pack ("H*", $transf_random_seed); $enc_iv = pack ("H*", $enc_iv); my $intermediate_hash = sha256 ($word); if ($version == 1) { $contents_hash = $salt_arr[6]; $contents_hash = pack ("H*", $contents_hash); $inline_flag = $salt_arr[7]; $contents_len = $salt_arr[8]; $contents = $salt_arr[9]; $contents = pack ("H*", $contents); # keyfile handling if (scalar @salt_arr == 13) { $inline_keyfile_flag = $salt_arr[10]; $keyfile_len = $salt_arr[11]; $keyfile_content = $salt_arr[12]; $keyfile_attributes = $keyfile_attributes . "*" . $inline_keyfile_flag . "*" . $keyfile_len . "*" . $keyfile_content; $intermediate_hash = $intermediate_hash . pack ("H*", $keyfile_content); $intermediate_hash = sha256 ($intermediate_hash); } } elsif ($version == 2) { # keyfile handling if (scalar @salt_arr == 11) { $inline_keyfile_flag = $salt_arr[8]; $keyfile_len = $salt_arr[9]; $keyfile_content = $salt_arr[10]; $intermediate_hash = $intermediate_hash . pack ("H*", $keyfile_content); $keyfile_attributes = $keyfile_attributes . "*" . $inline_keyfile_flag . "*" . $keyfile_len . "*" . $keyfile_content; } $intermediate_hash = sha256 ($intermediate_hash); } my $aes = Crypt::Mode::ECB->new ('AES', 1); for (my $j = 0; $j < $iteration; $j++) { $intermediate_hash = $aes->encrypt ($intermediate_hash, $transf_random_seed); $intermediate_hash = substr ($intermediate_hash, 0, 32); } $intermediate_hash = sha256 ($intermediate_hash); my $final_key = sha256 ($final_random_seed . $intermediate_hash); my $final_algorithm; if ($version == 1 && $algorithm == 1) { $final_algorithm = "Crypt::Twofish"; } else { $final_algorithm = "Crypt::Rijndael"; } my $cipher; if ($version == 1) { $cipher = Crypt::CBC->new ({ key => $final_key, cipher => $final_algorithm, iv => $enc_iv, literal_key => 1, header => "none", padding => "standard", keysize => 32 }); } else { $cipher = Crypt::CBC->new ({ key => $final_key, cipher => $final_algorithm, iv => $enc_iv, literal_key => 1, header => "none", padding => "none", keysize => 32 }); } my $hash; if ($version == 1) { if (defined $param) { # if we try to verify the crack, we need to decrypt the contents instead of only encrypting it: $contents = $cipher->decrypt ($contents); # and check the output my $contents_hash_old = $contents_hash; $contents_hash = sha256 ($contents); if ($contents_hash_old ne $contents_hash) { # fake content $contents = "\x00" x length ($contents); } } else { $contents_hash = sha256 ($contents); } $contents = $cipher->encrypt ($contents); $hash = sprintf ('$keepass$*%d*%d*%d*%s*%s*%s*%s*%d*%d*%s%s', $version, $iteration, $algorithm, unpack ("H*", $final_random_seed), unpack ("H*", $transf_random_seed), unpack ("H*", $enc_iv), unpack ("H*", $contents_hash), $inline_flag, $contents_len, unpack ("H*", $contents), $keyfile_attributes); } if ($version == 2) { $expected_bytes = $salt_arr[6]; $contents_hash = $salt_arr[7]; $contents_hash = pack ("H*", $contents_hash); $expected_bytes = $cipher->decrypt ($contents_hash); $expected_bytes = substr ($expected_bytes . "\x00" x 32, 0, 32); # padding $hash = sprintf ('$keepass$*%d*%d*%d*%s*%s*%s*%s*%s%s', $version, $iteration, $algorithm, unpack ("H*", $final_random_seed), unpack ("H*", $transf_random_seed), unpack ("H*", $enc_iv), unpack ("H*", $expected_bytes), unpack ("H*", $contents_hash), $keyfile_attributes); } return $hash; } sub module_verify_hash { my $line = shift; my ($hash_in, $word) = split ":", $line; return unless defined $hash_in; return unless defined $word; my @data = split ('\*', $hash_in); return unless (scalar @data == 9 || scalar @data == 11 || scalar @data == 12 || scalar @data == 14); my $signature = shift @data; return unless ($signature eq '$keepass$'); my $version = shift @data; return unless ($version == 1 || $version == 2); my $iteration = shift @data; my $algorithm = shift @data; my $final_random_seed = shift @data; if ($version == 1) { return unless (length ($final_random_seed) == 32); } elsif ($version == 2) { return unless (length ($final_random_seed) == 64); } my $transf_random_seed = shift @data; return unless (length ($transf_random_seed) == 64); my $enc_iv = shift @data; return unless (length ($enc_iv) == 32); if ($version == 1) { my $contents_hash = shift @data; return unless (length ($contents_hash) == 64); my $inline_flags = shift @data; return unless ($inline_flags == 1); my $contents_len = shift @data; my $contents = shift @data; return unless (length ($contents) == $contents_len * 2); } elsif ($version == 2) { my $expected_bytes = shift @data; return unless (length ($expected_bytes) == 64); my $contents_hash = shift @data; return unless (length ($contents_hash) == 64); } if (scalar @data == 12 || scalar @data == 14) { my $inline_flags = shift @data; return unless ($inline_flags == 1); my $keyfile_len = shift @data; return unless ($keyfile_len == 64); my $keyfile = shift @data; return unless (length ($keyfile) == $keyfile_len); } my $salt = substr ($hash_in, length ("*keepass*") + 1); my $param = 1; # distinguish between encrypting vs decrypting return unless defined $salt; $word = pack_if_HEX_notation ($word); my $new_hash = module_generate_hash ($word, $salt, $param); return ($new_hash, $word); } 1;