// Package kdbx reads, writes and merges KeePass 2 (KDBX 4) databases. // // It exists so a synchronization server can merge two edits of the same // vault instead of picking a winner. Merging means decrypting, which means // the holder of this code holds the vault's secrets: see the package's // Merge documentation and the server's handover notes for what that costs. // // The implementation is deliberately narrow. It reads and writes KDBX 4.0 // and 4.1 with AES-256-CBC or ChaCha20 payloads, AES-KDF or Argon2 key // derivation, and the ChaCha20 inner stream. It reports anything else as // unsupported rather than guessing, and it preserves every XML element it // does not understand. package kdbx import ( "encoding/binary" "errors" "fmt" ) const ( signature1 = 0x9AA2D903 signature2 = 0xB54BFB67 // Version4 is the file version this package writes: KDBX 4.0. Version4 uint32 = 4<<16 | 0 // version41 is also readable; 4.1 only adds XML elements, which the // generic tree carries through untouched. version41 uint32 = 4<<16 | 1 ) // Outer header field identifiers. const ( hdrEnd = 0 hdrComment = 1 hdrCipherID = 2 hdrCompression = 3 hdrMasterSeed = 4 hdrTransformSeed = 5 // KDBX 3 only hdrTransformRounds = 6 // KDBX 3 only hdrEncryptionIV = 7 hdrInnerStreamKey3 = 8 // KDBX 3 only hdrStreamStartBytes = 9 // KDBX 3 only hdrInnerStreamID3 = 10 // KDBX 3 only hdrKdfParameters = 11 hdrPublicCustomData = 12 ) // Inner header field identifiers. const ( innerEnd = 0 innerStreamID = 1 innerStreamKey = 2 innerBinary = 3 ) // Inner random stream identifiers. const ( streamNone = 0 streamSalsa20 = 2 streamChaCha20 = 3 ) // Cipher and KDF identifiers, stored as raw 16-byte UUIDs. var ( cipherAES256CBC = []byte{0x31, 0xC1, 0xF2, 0xE6, 0xBF, 0x71, 0x43, 0x50, 0xBE, 0x58, 0x05, 0x21, 0x6A, 0xFC, 0x5A, 0xFF} cipherChaCha20Outer = []byte{0xD6, 0x03, 0x8A, 0x2B, 0x8B, 0x6F, 0x4C, 0xB5, 0xA5, 0x24, 0x33, 0x9A, 0x31, 0xDB, 0xB5, 0x9A} kdfAES = []byte{0xC9, 0xD9, 0xF3, 0x9A, 0x62, 0x8A, 0x44, 0x60, 0xBF, 0x74, 0x0D, 0x08, 0xC1, 0x8A, 0x4F, 0xEA} kdfArgon2d = []byte{0xEF, 0x63, 0x6D, 0xDF, 0x8C, 0x29, 0x44, 0x4B, 0x91, 0xF7, 0xA9, 0xA4, 0x03, 0xE3, 0x0A, 0x0C} kdfArgon2id = []byte{0x9E, 0x29, 0x8B, 0x19, 0x56, 0xDB, 0x47, 0x73, 0xB2, 0x3D, 0xFC, 0x3E, 0xC6, 0xF0, 0xA1, 0xE1} ) // ErrWrongPassword is returned when the credentials do not open the file. var ErrWrongPassword = errors.New("kdbx: wrong password or corrupted file") // ErrUnsupported is returned for a file this package will not touch, such as // one using a cipher it cannot compute. A caller that cannot merge should // fall back to a non-merging strategy rather than lose data. type ErrUnsupported struct{ Reason string } func (e *ErrUnsupported) Error() string { return "kdbx: unsupported: " + e.Reason } func unsupported(format string, args ...any) error { return &ErrUnsupported{Reason: fmt.Sprintf(format, args...)} } // variantDict is KeePass's typed key-value container, used for the KDF // parameters and public custom data. Order is preserved so an unchanged // dictionary round-trips byte for byte. type variantDict struct { items []variantItem } type variantItem struct { typ byte key string value []byte } const ( vdUint32 = 0x04 vdUint64 = 0x05 vdBool = 0x08 vdInt32 = 0x0C vdInt64 = 0x0D vdString = 0x18 vdByteArray = 0x42 vdVersion = 0x0100 ) func parseVariantDict(b []byte) (variantDict, error) { var vd variantDict if len(b) < 2 { return vd, errors.New("kdbx: variant dictionary too short") } if v := binary.LittleEndian.Uint16(b); v>>8 != vdVersion>>8 { return vd, unsupported("variant dictionary version %#04x", v) } p := 2 for { if p >= len(b) { return vd, errors.New("kdbx: variant dictionary is not terminated") } typ := b[p] p++ if typ == 0 { return vd, nil } if p+4 > len(b) { return vd, errors.New("kdbx: truncated variant dictionary key") } kl := int(binary.LittleEndian.Uint32(b[p:])) p += 4 if kl < 0 || p+kl > len(b) { return vd, errors.New("kdbx: truncated variant dictionary key") } key := string(b[p : p+kl]) p += kl if p+4 > len(b) { return vd, errors.New("kdbx: truncated variant dictionary value") } vl := int(binary.LittleEndian.Uint32(b[p:])) p += 4 if vl < 0 || p+vl > len(b) { return vd, errors.New("kdbx: truncated variant dictionary value") } val := make([]byte, vl) copy(val, b[p:p+vl]) p += vl vd.items = append(vd.items, variantItem{typ: typ, key: key, value: val}) } } func (vd variantDict) marshal() []byte { out := make([]byte, 2) binary.LittleEndian.PutUint16(out, vdVersion) for _, it := range vd.items { out = append(out, it.typ) out = binary.LittleEndian.AppendUint32(out, uint32(len(it.key))) out = append(out, it.key...) out = binary.LittleEndian.AppendUint32(out, uint32(len(it.value))) out = append(out, it.value...) } return append(out, 0) } func (vd variantDict) get(key string) (variantItem, bool) { for _, it := range vd.items { if it.key == key { return it, true } } return variantItem{}, false } func (vd variantDict) bytes(key string) []byte { if it, ok := vd.get(key); ok { return it.value } return nil } func (vd variantDict) uint64(key string) (uint64, bool) { it, ok := vd.get(key) if !ok { return 0, false } switch { case len(it.value) == 8: return binary.LittleEndian.Uint64(it.value), true case len(it.value) == 4: return uint64(binary.LittleEndian.Uint32(it.value)), true } return 0, false } func (vd *variantDict) set(typ byte, key string, value []byte) { for i := range vd.items { if vd.items[i].key == key { vd.items[i].typ, vd.items[i].value = typ, value return } } vd.items = append(vd.items, variantItem{typ: typ, key: key, value: value}) } func (vd *variantDict) setUint64(key string, v uint64) { vd.set(vdUint64, key, binary.LittleEndian.AppendUint64(nil, v)) } func (vd *variantDict) setUint32(key string, v uint32) { vd.set(vdUint32, key, binary.LittleEndian.AppendUint32(nil, v)) }