├── LICENSE ├── README.md └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Javed Khan 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | kpx 2 | === 3 | 4 | `kpx /path/to/keepassx.db` 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "os" 14 | "os/signal" 15 | "strings" 16 | "time" 17 | 18 | "golang.org/x/crypto/ssh/terminal" 19 | ) 20 | 21 | // SYS_USR_ID is a reserved entry id 22 | // It is used to skip processing system entries 23 | const SYS_USR_ID = uint32(0) 24 | 25 | // Sha256 returns sha256 hash of the given data 26 | func Sha256(k []byte) []byte { 27 | hash := sha256.New() 28 | hash.Write(k) 29 | return hash.Sum(nil) 30 | } 31 | 32 | // ErrInvalidEncryptionFlag is returned on an invalid encryption flag 33 | var ErrInvalidEncryptionFlag error 34 | 35 | // EncryptionTypes maps supported encryption types and the flags 36 | var EncryptionTypes = map[string]uint32{ 37 | // TODO: Support these 38 | //"SHA2": 1, 39 | //"AES": 2, 40 | "Rijndael": 2, 41 | "ArcFour": 4, 42 | "TwoFish": 8, 43 | } 44 | 45 | // ParseError is raised when there is an error during parsing of the payload 46 | var ParseError = errors.New("unable to parse payload") 47 | 48 | type BaseType struct{} 49 | 50 | func (b BaseType) Decode(payload []byte) interface{} { 51 | return payload 52 | } 53 | 54 | type StringType struct{} 55 | 56 | func (s StringType) Decode(payload []byte) string { 57 | return strings.TrimRight(string(payload[:]), "\x00") 58 | } 59 | 60 | type IntegerType struct{} 61 | 62 | func (i IntegerType) Decode(payload []byte) uint32 { 63 | return binary.LittleEndian.Uint32(payload) 64 | } 65 | 66 | type ShortType struct{} 67 | 68 | func (s ShortType) Decode(payload []byte) uint16 { 69 | return binary.LittleEndian.Uint16(payload) 70 | } 71 | 72 | type UUIDType struct{} 73 | 74 | func (u UUIDType) Decode(payload []byte) interface{} { 75 | return strings.TrimRight(string(payload[:]), "\x00") 76 | } 77 | 78 | type DateType struct{} 79 | 80 | func (d DateType) Decode(payload []byte) interface{} { 81 | year := int((uint16(payload[0]) << 6) | (uint16(payload[1]) >> 2)) 82 | month := int(((payload[1] & 0x00000003) << 2) | (payload[2] >> 6)) 83 | day := int((payload[2] >> 1) & 0x0000001F) 84 | hour := int(((payload[2] & 0x00000001) << 4) | (payload[3] >> 4)) 85 | minutes := int(((payload[3] & 0x0000000F) << 2) | (payload[4] >> 6)) 86 | seconds := int(payload[4] & 0x0000003F) 87 | return time.Date(year, time.Month(month), day, hour, minutes, seconds, 0, time.UTC) 88 | } 89 | 90 | // Group represents a KeepassX entries group. 91 | type Group struct { 92 | ignored bool 93 | id uint32 94 | name string 95 | imageid uint32 96 | level uint16 97 | flags uint32 98 | } 99 | 100 | // Entry represents a KeepassX entry. 101 | type Entry struct { 102 | id uint32 103 | groupid uint32 104 | group *Group 105 | imageid uint32 106 | title string 107 | url string 108 | username string 109 | password string 110 | ignored bool 111 | notes string 112 | creation_time time.Time 113 | last_mod_time time.Time 114 | last_acc_time time.Time 115 | expiration_time time.Time 116 | binary_desc string 117 | binary_data []byte 118 | } 119 | 120 | // Metadata is the metadata stored in the KeepassX database. 121 | type Metadata struct { 122 | signature1 uint32 123 | signature2 uint32 124 | flags uint32 125 | version uint32 126 | seed [16]byte 127 | iv [16]byte 128 | groups uint32 129 | entries uint32 130 | hash [32]byte 131 | seed2 [32]byte 132 | rounds uint32 133 | } 134 | 135 | // KeepassXDatabase is the KeepassX database. 136 | type KeepassXDatabase struct { 137 | *Metadata 138 | password []byte 139 | keyfile string 140 | payload []byte 141 | groupIdx map[uint32]*Group 142 | results map[uint32][]Entry 143 | } 144 | 145 | // NewKeepassXDatabase returns an instance of KeepassXDatabase from the given 146 | // password and keyfile. 147 | func NewKeepassXDatabase(password []byte, keyfile string) (*KeepassXDatabase, error) { 148 | return &KeepassXDatabase{ 149 | Metadata: new(Metadata), 150 | password: password, 151 | keyfile: keyfile, 152 | groupIdx: make(map[uint32]*Group), 153 | }, nil 154 | } 155 | 156 | // ReadFrom reads the given reader and loads the metadata into memory. 157 | func (m *Metadata) ReadFrom(r io.Reader) (int64, error) { 158 | var buf [4]byte 159 | uint32Bytes := buf[:4] 160 | 161 | n, err := io.ReadFull(r, uint32Bytes) 162 | if err != nil { 163 | return 0, err 164 | } 165 | n64 := int64(n) 166 | m.signature1 = binary.LittleEndian.Uint32(uint32Bytes) 167 | 168 | n, err = io.ReadFull(r, uint32Bytes) 169 | if err != nil { 170 | return 0, err 171 | } 172 | n64 += int64(n) 173 | m.signature2 = binary.LittleEndian.Uint32(uint32Bytes) 174 | 175 | n, err = io.ReadFull(r, uint32Bytes) 176 | if err != nil { 177 | return 0, err 178 | } 179 | n64 += int64(n) 180 | m.flags = binary.LittleEndian.Uint32(uint32Bytes) 181 | 182 | n, err = io.ReadFull(r, uint32Bytes) 183 | if err != nil { 184 | return 0, err 185 | } 186 | n64 += int64(n) 187 | m.version = binary.LittleEndian.Uint32(uint32Bytes) 188 | 189 | var seed [16]byte 190 | n, err = io.ReadFull(r, seed[:]) 191 | if err != nil { 192 | return 0, err 193 | } 194 | n64 += int64(n) 195 | m.seed = seed 196 | 197 | var encryption [16]byte 198 | n, err = io.ReadFull(r, encryption[:]) 199 | if err != nil { 200 | return 0, err 201 | } 202 | n64 += int64(n) 203 | m.iv = encryption 204 | 205 | n, err = io.ReadFull(r, uint32Bytes) 206 | if err != nil { 207 | return 0, err 208 | } 209 | n64 += int64(n) 210 | m.groups = binary.LittleEndian.Uint32(uint32Bytes) 211 | 212 | n, err = io.ReadFull(r, uint32Bytes) 213 | if err != nil { 214 | return 0, err 215 | } 216 | n64 += int64(n) 217 | m.entries = binary.LittleEndian.Uint32(uint32Bytes) 218 | 219 | var hash [32]byte 220 | n, err = io.ReadFull(r, hash[:]) 221 | if err != nil { 222 | return 0, err 223 | } 224 | n64 += int64(n) 225 | m.hash = hash 226 | 227 | var seed2 [32]byte 228 | n, err = io.ReadFull(r, seed2[:]) 229 | if err != nil { 230 | return 0, err 231 | } 232 | n64 += int64(n) 233 | m.seed2 = seed2 234 | 235 | n, err = io.ReadFull(r, uint32Bytes) 236 | if err != nil { 237 | return 0, err 238 | } 239 | n64 += int64(n) 240 | m.rounds = binary.LittleEndian.Uint32(uint32Bytes) 241 | 242 | return n64, nil 243 | } 244 | 245 | // getEncryptionFlag returns the encryption type flag. 246 | func getEncryptionFlag(flag uint32) (string, error) { 247 | for k, v := range EncryptionTypes { 248 | if v&flag != 0 { 249 | return k, nil 250 | } 251 | } 252 | // invalid flag 253 | return "", ErrInvalidEncryptionFlag 254 | } 255 | 256 | // decryptPayload decrypts the given payload. 257 | func (k *KeepassXDatabase) decryptPayload(content []byte, key []byte, 258 | encryption_type string, iv [16]byte) ([]byte, error) { 259 | data := make([]byte, len(content)) 260 | if encryption_type != "Rijndael" { 261 | // Only Rijndael is supported atm. 262 | return data, errors.New(fmt.Sprintf("Unsupported encryption type: %s", 263 | encryption_type)) 264 | } 265 | decryptor, err := aes.NewCipher(key) 266 | if err != nil { 267 | return data, err 268 | } 269 | // Block mode CBC 270 | mode := cipher.NewCBCDecrypter(decryptor, iv[:]) 271 | mode.CryptBlocks(data, content) 272 | return data, err 273 | } 274 | 275 | // calculateKey calculates the key required to decrypt the payload. 276 | func (k *KeepassXDatabase) calculateKey() ([]byte, error) { 277 | // TODO: support keyfile 278 | key := Sha256(k.password) 279 | cipher, err := aes.NewCipher(k.seed2[:]) 280 | if err != nil { 281 | return key, err 282 | } 283 | // divide key into half and encrypt with cipher 284 | for i := 0; i < int(k.rounds); i++ { 285 | cipher.Encrypt(key[:16], key[:16]) 286 | cipher.Encrypt(key[16:], key[16:]) 287 | } 288 | key = Sha256(key) 289 | return Sha256(append(k.seed[:], key...)), nil 290 | } 291 | 292 | // parsePayload parses the payload and returns the results as a map. 293 | func (k *KeepassXDatabase) parsePayload(payload []byte) (map[uint32][]Entry, error) { 294 | groups, offset, err := k.parseGroups(payload) 295 | if err != nil { 296 | return nil, err 297 | } 298 | for i := 0; i < len(groups); i++ { 299 | k.groupIdx[groups[i].id] = &groups[i] 300 | } 301 | entries, err := k.parseEntries(payload[offset:]) 302 | if err != nil { 303 | return nil, err 304 | } 305 | results := make(map[uint32][]Entry) 306 | for _, entry := range entries { 307 | results[entry.groupid] = append(results[entry.groupid], entry) 308 | } 309 | return results, nil 310 | } 311 | 312 | // getGroup returns the group for the given group id. 313 | func (k *KeepassXDatabase) getGroup(id uint32) (*Group, error) { 314 | g, ok := k.groupIdx[id] 315 | if ok { 316 | return g, nil 317 | } 318 | return nil, errors.New("group not found") 319 | } 320 | 321 | // parseEntries parses the payload and returns an array of entries. 322 | func (k *KeepassXDatabase) parseEntries(payload []byte) ([]Entry, error) { 323 | offset := 0 324 | var entries []Entry 325 | for i := 0; i < int(k.entries); i++ { 326 | var e Entry 327 | out: 328 | for { 329 | field_type := binary.LittleEndian.Uint16(payload[offset : offset+2]) 330 | offset += 2 331 | field_size := int(binary.LittleEndian.Uint32(payload[offset : offset+4])) 332 | offset += 4 333 | switch field_type { 334 | case 0x1: 335 | s := IntegerType{} 336 | data := payload[offset : offset+field_size] 337 | offset += field_size 338 | e.id = s.Decode(data) 339 | case 0x2: 340 | s := IntegerType{} 341 | data := payload[offset : offset+field_size] 342 | offset += field_size 343 | e.groupid = s.Decode(data) 344 | group, err := k.getGroup(e.groupid) 345 | if err != nil { 346 | group = nil 347 | } 348 | e.group = group 349 | case 0x3: 350 | s := IntegerType{} 351 | data := payload[offset : offset+field_size] 352 | offset += field_size 353 | e.imageid = s.Decode(data) 354 | case 0x4: 355 | s := StringType{} 356 | data := payload[offset : offset+field_size] 357 | offset += field_size 358 | e.title = s.Decode(data) 359 | case 0x5: 360 | s := StringType{} 361 | data := payload[offset : offset+field_size] 362 | offset += field_size 363 | e.url = s.Decode(data) 364 | case 0x6: 365 | s := StringType{} 366 | data := payload[offset : offset+field_size] 367 | offset += field_size 368 | e.username = s.Decode(data) 369 | case 0x7: 370 | s := StringType{} 371 | data := payload[offset : offset+field_size] 372 | offset += field_size 373 | e.password = s.Decode(data) 374 | case 0x8: 375 | s := StringType{} 376 | data := payload[offset : offset+field_size] 377 | offset += field_size 378 | e.notes = s.Decode(data) 379 | case 0x9: 380 | d := DateType{} 381 | data := payload[offset : offset+field_size] 382 | offset += field_size 383 | i := d.Decode(data) 384 | e.creation_time = i.(time.Time) 385 | case 0xa: 386 | d := DateType{} 387 | data := payload[offset : offset+field_size] 388 | offset += field_size 389 | i := d.Decode(data) 390 | e.last_mod_time = i.(time.Time) 391 | case 0xb: 392 | d := DateType{} 393 | data := payload[offset : offset+field_size] 394 | offset += field_size 395 | i := d.Decode(data) 396 | e.last_acc_time = i.(time.Time) 397 | case 0xc: 398 | d := DateType{} 399 | data := payload[offset : offset+field_size] 400 | offset += field_size 401 | i := d.Decode(data) 402 | e.expiration_time = i.(time.Time) 403 | case 0xd: 404 | s := StringType{} 405 | data := payload[offset : offset+field_size] 406 | offset += field_size 407 | e.binary_desc = s.Decode(data) 408 | case 0xe: 409 | b := BaseType{} 410 | data := payload[offset : offset+field_size] 411 | offset += field_size 412 | i := b.Decode(data) 413 | e.binary_data = i.([]byte) 414 | case 0xffff: 415 | break out 416 | } 417 | } 418 | // SYS_USR_ID is reserved for system entries 419 | if e.id != SYS_USR_ID { 420 | entries = append(entries, e) 421 | } 422 | } 423 | return entries, nil 424 | } 425 | 426 | // parseGroups parses the given payload and returns an array of groups. 427 | func (k *KeepassXDatabase) parseGroups(payload []byte) ([]Group, int, error) { 428 | offset := 0 429 | var groups []Group 430 | for i := 0; i < int(k.groups); i++ { 431 | var g Group 432 | out: 433 | for { 434 | // Must be able to read the next two bytes 435 | if offset+2 > len(payload) { 436 | return nil, 0, ParseError 437 | } 438 | field_type := binary.LittleEndian.Uint16(payload[offset : offset+2]) 439 | offset += 2 440 | field_size := int(binary.LittleEndian.Uint32(payload[offset : offset+4])) 441 | offset += 4 442 | switch field_type { 443 | case 0x1: 444 | s := IntegerType{} 445 | data := payload[offset : offset+field_size] 446 | offset += field_size 447 | g.id = s.Decode(data) 448 | case 0x2: 449 | s := StringType{} 450 | data := payload[offset : offset+field_size] 451 | offset += field_size 452 | g.name = s.Decode(data) 453 | case 0x7: 454 | s := IntegerType{} 455 | data := payload[offset : offset+field_size] 456 | offset += field_size 457 | g.imageid = s.Decode(data) 458 | case 0x8: 459 | s := ShortType{} 460 | data := payload[offset : offset+field_size] 461 | offset += field_size 462 | g.level = s.Decode(data) 463 | case 0x9: 464 | s := IntegerType{} 465 | data := payload[offset : offset+field_size] 466 | offset += field_size 467 | g.flags = s.Decode(data) 468 | case 0xffff: 469 | break out 470 | } 471 | } 472 | groups = append(groups, g) 473 | } 474 | return groups, offset, nil 475 | } 476 | 477 | // ReadFrom reads the given reader and loads the keepassx database file into 478 | // memory. 479 | func (k *KeepassXDatabase) ReadFrom(r io.Reader) (int64, error) { 480 | n, err := k.Metadata.ReadFrom(r) 481 | if err != nil { 482 | return n, err 483 | } 484 | content, err := ioutil.ReadAll(r) 485 | if err != nil { 486 | return n, err 487 | } 488 | encryption_type, err := getEncryptionFlag(k.flags) 489 | if err != nil { 490 | return n, err 491 | } 492 | key, err := k.calculateKey() 493 | if err != nil { 494 | return n, err 495 | } 496 | payload, err := k.decryptPayload(content, key, encryption_type, k.iv) 497 | if err != nil { 498 | return n, err 499 | } 500 | results, err := k.parsePayload(payload) 501 | if err != nil { 502 | return n, err 503 | } 504 | k.results = results 505 | return n, err 506 | } 507 | 508 | func main() { 509 | var path string 510 | if len(os.Args) > 1 { 511 | path = os.Args[1] 512 | } 513 | // Print help 514 | if path == "" || path == "-h" || path == "--help" { 515 | log.Printf("Usage: kpx ") 516 | return 517 | } 518 | if !strings.HasSuffix(path, ".kdb") { 519 | log.Fatal("unknown file format") 520 | } 521 | var keyfile string 522 | if len(os.Args) > 2 { 523 | keyfile = os.Args[2] 524 | } 525 | f, err := os.OpenFile(path, os.O_RDONLY, 0) 526 | defer f.Close() 527 | if err != nil { 528 | log.Fatalf("%v", err) 529 | } 530 | 531 | // Handle interrupts when reading password 532 | c := make(chan os.Signal, 1) 533 | signal.Notify(c, os.Interrupt) 534 | 535 | fmt.Print("Password: ") 536 | password, err := terminal.ReadPassword(int(os.Stdin.Fd())) 537 | if err != nil { 538 | log.Fatalf("%v", err) 539 | } 540 | fmt.Print("\n") 541 | db, err := NewKeepassXDatabase(password, keyfile) 542 | if err != nil { 543 | log.Fatalf("%v", err) 544 | } 545 | _, err = db.ReadFrom(f) 546 | if err != nil { 547 | log.Fatalf("%v", err) 548 | } 549 | // Write the results to stdout 550 | for id, entries := range db.results { 551 | group, err := db.getGroup(id) 552 | if err != nil { 553 | log.Fatalf("%v", err) 554 | } 555 | fmt.Printf("===== %v ======\n", group.name) 556 | for i, entry := range entries { 557 | fmt.Printf("%v | %v | %v | %v\n", entry.id, i, entry.title, entry.url) 558 | } 559 | fmt.Printf("===== x ======\n") 560 | } 561 | } 562 | --------------------------------------------------------------------------------