mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-01-23 05:01:19 -05:00
add challenge-response recovery tool (see keepassxreboot/keepassxc#1734)
This commit is contained in:
parent
06e5f19fab
commit
0c252b6ed4
1
utils/keepassxc-cr-recovery/.gitignore
vendored
Normal file
1
utils/keepassxc-cr-recovery/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
keepass-cr-recovery
|
20
utils/keepassxc-cr-recovery/README.md
Normal file
20
utils/keepassxc-cr-recovery/README.md
Normal file
@ -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.
|
5
utils/keepassxc-cr-recovery/go.mod
Normal file
5
utils/keepassxc-cr-recovery/go.mod
Normal file
@ -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
|
8
utils/keepassxc-cr-recovery/go.sum
Normal file
8
utils/keepassxc-cr-recovery/go.sum
Normal file
@ -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=
|
182
utils/keepassxc-cr-recovery/main.go
Normal file
182
utils/keepassxc-cr-recovery/main.go
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user