keepassxc/utils/keepassxc-cr-recovery/main.go

183 lines
3.8 KiB
Go

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)
}
}