From 0c252b6ed4c5f54abcdbb81c7ecb124a905138f7 Mon Sep 17 00:00:00 2001 From: Julian Einwag Date: Wed, 22 Jan 2020 19:14:49 +0100 Subject: [PATCH] add challenge-response recovery tool (see keepassxreboot/keepassxc#1734) --- utils/keepassxc-cr-recovery/.gitignore | 1 + utils/keepassxc-cr-recovery/README.md | 20 +++ utils/keepassxc-cr-recovery/go.mod | 5 + utils/keepassxc-cr-recovery/go.sum | 8 ++ utils/keepassxc-cr-recovery/main.go | 182 +++++++++++++++++++++++++ 5 files changed, 216 insertions(+) create mode 100644 utils/keepassxc-cr-recovery/.gitignore create mode 100644 utils/keepassxc-cr-recovery/README.md create mode 100644 utils/keepassxc-cr-recovery/go.mod create mode 100644 utils/keepassxc-cr-recovery/go.sum create mode 100644 utils/keepassxc-cr-recovery/main.go diff --git a/utils/keepassxc-cr-recovery/.gitignore b/utils/keepassxc-cr-recovery/.gitignore new file mode 100644 index 000000000..01d743bff --- /dev/null +++ b/utils/keepassxc-cr-recovery/.gitignore @@ -0,0 +1 @@ +keepass-cr-recovery diff --git a/utils/keepassxc-cr-recovery/README.md b/utils/keepassxc-cr-recovery/README.md new file mode 100644 index 000000000..d6e3fef11 --- /dev/null +++ b/utils/keepassxc-cr-recovery/README.md @@ -0,0 +1,20 @@ +# keepassxc-cr-recovery + +A small tool that helps you regain access to your KeePassXC password database in case you have it protected with YubiKey challenge-response and lost your key. +Currently supports KDBX4 databases with Argon2 hashing. + +## Building + +Tested with Go 1.13. Just run `go build`. + +## Usage + +What you need: +* your KeePassXC database +* your challenge-response secret. This cannot be retrieved from the YubiKey, it needs to be saved upon initial configuration of the key. + +Then just run +```shell +keepass-cr-recovery path-to-your-password-database path-of-the-new-keyfile +``` +It will prompt for the challenge-response secret. You will get a keyfile at the specified destination path. Then, to unlock your database in KeePassXC, you need to check "key file" instead of "challenge response" and load the file. \ No newline at end of file diff --git a/utils/keepassxc-cr-recovery/go.mod b/utils/keepassxc-cr-recovery/go.mod new file mode 100644 index 000000000..89afe5e32 --- /dev/null +++ b/utils/keepassxc-cr-recovery/go.mod @@ -0,0 +1,5 @@ +module github.com/keepassxreboot/keepassxc/keepassxc-cr-recovery + +go 1.13 + +require golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 diff --git a/utils/keepassxc-cr-recovery/go.sum b/utils/keepassxc-cr-recovery/go.sum new file mode 100644 index 000000000..452e5b0ad --- /dev/null +++ b/utils/keepassxc-cr-recovery/go.sum @@ -0,0 +1,8 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876 h1:sKJQZMuxjOAR/Uo2LBfU90onWEf1dF4C+0hPJCc9Mpc= +golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/utils/keepassxc-cr-recovery/main.go b/utils/keepassxc-cr-recovery/main.go new file mode 100644 index 000000000..b9e64d3ed --- /dev/null +++ b/utils/keepassxc-cr-recovery/main.go @@ -0,0 +1,182 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha1" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "syscall" + + "encoding/binary" + "encoding/hex" + + "golang.org/x/crypto/ssh/terminal" +) + +const fileVersionCriticalMask uint32 = 0xFFFF0000 +const argon2Salt = "S" +const endOfHeader = 0 +const endOfVariantMap = 0 +const kdfParameters = 11 + +func readSecret() (string, error) { + fmt.Print("Secret: ") + byteSecret, err := terminal.ReadPassword(int(syscall.Stdin)) + fmt.Println() + secret := string(byteSecret) + return secret, err + +} +func readHeaderField(reader io.Reader) (bool, byte, []byte, error) { + var fieldID byte + err := binary.Read(reader, binary.LittleEndian, &fieldID) + if err != nil { + return true, 0, nil, err + } + + if fieldID == endOfHeader { + return false, 0, nil, nil + } + + var fieldLength uint32 + err = binary.Read(reader, binary.LittleEndian, &fieldLength) + if err != nil { + return true, fieldID, nil, err + } + + fieldData := make([]byte, fieldLength) + err = binary.Read(reader, binary.LittleEndian, &fieldData) + if err != nil { + return true, fieldID, fieldData, err + } + return true, fieldID, fieldData, nil +} +func readVariantMap(reader io.Reader) ([]byte, error) { + var version uint16 + err := binary.Read(reader, binary.LittleEndian, &version) + if err != nil { + return nil, err + } + + var fieldType byte + for err = binary.Read(reader, binary.LittleEndian, &fieldType); fieldType != endOfVariantMap && err == nil; err = binary.Read(reader, binary.LittleEndian, &fieldType) { + + var nameLen uint32 + err = binary.Read(reader, binary.LittleEndian, &nameLen) + if err != nil { + return nil, err + } + + nameBytes := make([]byte, nameLen) + err = binary.Read(reader, binary.LittleEndian, &nameBytes) + if err != nil { + return nil, err + } + + name := string(nameBytes) + + var valueLen uint32 + err = binary.Read(reader, binary.LittleEndian, &valueLen) + if err != nil { + return nil, err + } + + value := make([]byte, valueLen) + err = binary.Read(reader, binary.LittleEndian, &value) + if err != nil { + return nil, err + } + + if name == argon2Salt { + return value, nil + } + } + return nil, nil +} +func readKeepassHeader(keepassFilename string) ([]byte, error) { + dbFile, err := os.Open(keepassFilename) + defer dbFile.Close() + if err != nil { + return nil, err + } + + var sig1, sig2, version uint32 + err = binary.Read(dbFile, binary.LittleEndian, &sig1) + if err != nil { + return nil, err + } + + err = binary.Read(dbFile, binary.LittleEndian, &sig2) + if err != nil { + return nil, err + } + + err = binary.Read(dbFile, binary.LittleEndian, &version) + if err != nil { + return nil, err + } + + version &= fileVersionCriticalMask + + var fieldData []byte + var fieldID byte + var moreFields bool + + for moreFields, fieldID, fieldData, err = readHeaderField(dbFile); moreFields && err == nil && fieldID != kdfParameters; moreFields, fieldID, fieldData, err = readHeaderField(dbFile) { + } + if err != nil { + return nil, err + } + + fieldReader := bytes.NewReader(fieldData) + seed, err := readVariantMap(fieldReader) + if err != nil { + return nil, err + } + return seed, nil + +} +func main() { + log.SetFlags(0) + args := os.Args + + if len(args) != 3 { + log.Fatalf("usage: %s keepassxc-database keyfile", args[0]) + } + + dbFilename := args[1] + keyFilename := args[2] + + if _, err := os.Stat(keyFilename); err == nil { + log.Fatalf("keyfile already exists, exiting") + } + secretHex, err := readSecret() + if err != nil { + log.Fatalf("couldn't read secret from stdin: %s", err) + } + secret, err := hex.DecodeString(secretHex) + + if err != nil { + log.Fatalf("couldn't decode secret: %s", err) + } + + challenge, err := readKeepassHeader(dbFilename) + if err != nil { + log.Fatalf("couldn't read challenge: %s", err) + } + + mac := hmac.New(sha1.New, secret) + mac.Write(challenge) + + hash := mac.Sum(nil) + + err = ioutil.WriteFile(keyFilename, hash, 0644) + if err != nil { + log.Fatalf("couldn't write keyfile: %s", err) + } + +}