Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/utils
diff options
context:
space:
mode:
authorJulian Einwag <jeinwag@users.noreply.github.com>2020-01-22 21:14:49 +0300
committerJonathan White <support@dmapps.us>2020-01-28 01:55:53 +0300
commit0c252b6ed4c5f54abcdbb81c7ecb124a905138f7 (patch)
tree0876dd4063d74da1e87a0db0081a1d600524c652 /utils
parent06e5f19fabeca49f69f703284aa8741a8f02c5d4 (diff)
add challenge-response recovery tool (see keepassxreboot/keepassxc#1734)
Diffstat (limited to 'utils')
-rw-r--r--utils/keepassxc-cr-recovery/.gitignore1
-rw-r--r--utils/keepassxc-cr-recovery/README.md20
-rw-r--r--utils/keepassxc-cr-recovery/go.mod5
-rw-r--r--utils/keepassxc-cr-recovery/go.sum8
-rw-r--r--utils/keepassxc-cr-recovery/main.go182
5 files changed, 216 insertions, 0 deletions
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)
+ }
+
+}