package kdbx import ( "encoding/base64" "encoding/binary" "time" ) // Generator is the name this package writes into a file's Meta. const Generator = "KeepOrg" // epochOffset converts between the Unix epoch and KDBX's, which counts // seconds from 0001-01-01 00:00:00 UTC. const epochOffset = 62135596800 // EncodeTime renders a timestamp the way KDBX 4 stores it: a little-endian // int64 of seconds since year one, base64 encoded. func EncodeTime(t time.Time) string { var b [8]byte binary.LittleEndian.PutUint64(b[:], uint64(t.UTC().Unix()+epochOffset)) return base64.StdEncoding.EncodeToString(b[:]) } // DecodeTime reads either KDBX 4's binary form or KDBX 3's ISO 8601 text, // because a file may have been written by either and merged here. func DecodeTime(s string) (time.Time, bool) { if s == "" { return time.Time{}, false } if raw, err := base64.StdEncoding.DecodeString(s); err == nil && len(raw) == 8 { secs := int64(binary.LittleEndian.Uint64(raw)) - epochOffset return time.Unix(secs, 0).UTC(), true } if t, err := time.Parse(time.RFC3339, s); err == nil { return t.UTC(), true } return time.Time{}, false } // NewUUID returns a fresh KDBX object identifier: 16 random bytes in base64, // which is how the format stores them. func NewUUID() string { return base64.StdEncoding.EncodeToString(randomBytes(16)) } var zeroUUID = base64.StdEncoding.EncodeToString(make([]byte, 16)) // New builds an empty database: one root group, no entries, ready to be // marshalled and handed to a KeePass application. func New(name string, kdf KDFParams) *Database { now := EncodeTime(time.Now()) db := &Database{ Version: Version4, Cipher: cipherAES256CBC, Compression: 1, KDF: newArgon2Params(kdf), } meta := &Node{Name: "Meta", Nodes: []*Node{ {Name: "Generator", Text: Generator}, {Name: "DatabaseName", Text: name}, {Name: "DatabaseNameChanged", Text: now}, {Name: "DatabaseDescription"}, {Name: "DatabaseDescriptionChanged", Text: now}, {Name: "DefaultUserName"}, {Name: "DefaultUserNameChanged", Text: now}, {Name: "MaintenanceHistoryDays", Text: "365"}, {Name: "Color"}, {Name: "MasterKeyChanged", Text: now}, {Name: "MasterKeyChangeRec", Text: "-1"}, {Name: "MasterKeyChangeForce", Text: "-1"}, {Name: "MemoryProtection", Nodes: []*Node{ {Name: "ProtectTitle", Text: "False"}, {Name: "ProtectUserName", Text: "False"}, {Name: "ProtectPassword", Text: "True"}, {Name: "ProtectURL", Text: "False"}, {Name: "ProtectNotes", Text: "False"}, }}, {Name: "RecycleBinEnabled", Text: "True"}, {Name: "RecycleBinUUID", Text: zeroUUID}, {Name: "RecycleBinChanged", Text: now}, {Name: "EntryTemplatesGroup", Text: zeroUUID}, {Name: "EntryTemplatesGroupChanged", Text: now}, {Name: "LastSelectedGroup", Text: zeroUUID}, {Name: "LastTopVisibleGroup", Text: zeroUUID}, {Name: "HistoryMaxItems", Text: "10"}, {Name: "HistoryMaxSize", Text: "6291456"}, {Name: "SettingsChanged", Text: now}, {Name: "CustomData"}, }} rootGroup := NewGroup(name) root := &Node{Name: "Root", Nodes: []*Node{ rootGroup, {Name: "DeletedObjects"}, }} db.Root = &Node{Name: "KeePassFile", Nodes: []*Node{meta, root}} return db } // NewGroup builds an empty group node. func NewGroup(name string) *Node { return &Node{Name: "Group", Nodes: []*Node{ {Name: "UUID", Text: NewUUID()}, {Name: "Name", Text: name}, {Name: "Notes"}, {Name: "IconID", Text: "48"}, newTimes(), {Name: "IsExpanded", Text: "True"}, {Name: "DefaultAutoTypeSequence"}, {Name: "EnableAutoType", Text: "null"}, {Name: "EnableSearching", Text: "null"}, {Name: "LastTopVisibleEntry", Text: zeroUUID}, }} } // NewEntry builds an entry with the standard string fields. The password is // marked protected, so it is stored under the inner stream cipher. func NewEntry(title, username, password, url, notes string) *Node { str := func(key, value string, protected bool) *Node { v := &Node{Name: "Value", Text: value, Protected: protected} return &Node{Name: "String", Nodes: []*Node{{Name: "Key", Text: key}, v}} } return &Node{Name: "Entry", Nodes: []*Node{ {Name: "UUID", Text: NewUUID()}, {Name: "IconID", Text: "0"}, {Name: "ForegroundColor"}, {Name: "BackgroundColor"}, {Name: "OverrideURL"}, {Name: "Tags"}, newTimes(), str("Title", title, false), str("UserName", username, false), str("Password", password, true), str("URL", url, false), str("Notes", notes, false), {Name: "AutoType", Nodes: []*Node{ {Name: "Enabled", Text: "True"}, {Name: "DataTransferObfuscation", Text: "0"}, }}, }} } func newTimes() *Node { now := EncodeTime(time.Now()) return &Node{Name: "Times", Nodes: []*Node{ {Name: "LastModificationTime", Text: now}, {Name: "CreationTime", Text: now}, {Name: "LastAccessTime", Text: now}, {Name: "ExpiryTime", Text: now}, {Name: "Expires", Text: "False"}, {Name: "UsageCount", Text: "0"}, {Name: "LocationChanged", Text: now}, }} } // RootGroup returns the database's top-level group. func (d *Database) RootGroup() *Node { return d.Root.Path("Root", "Group") } // Name is the database's display name from its Meta. func (d *Database) Name() string { return d.Root.Value("Meta", "DatabaseName") }