4 Commity ae0a6de15d ... 50667925da

Autor SHA1 Wiadomość Data
  maxep 50667925da Add hight-level KeePass protocols and type-erase 6 lat temu
  maxep a4b5fa97e7 Update KDB Tree creation 6 lat temu
  maxep 16d3f159b4 Bytes comply to Hashable 6 lat temu
  maxep 1c8b4eed28 Add TLV protocol for sequence extensions 6 lat temu

+ 22 - 1
Sources/Binary/Bytes.swift

@@ -231,4 +231,25 @@ extension Bytes: DataProtocol {
         rawValue.endIndex
     }
 
-}
+}
+
+extension Bytes: Hashable {
+
+    /// Hashes the essential components of this value by feeding them into the
+    /// given hasher.
+    ///
+    /// Implement this method to conform to the `Hashable` protocol. The
+    /// components used for hashing must be the same as the components compared
+    /// in your type's `==` operator implementation. Call `hasher.combine(_:)`
+    /// with each of these components.
+    ///
+    /// - Important: Never call `finalize()` on `hasher`. Doing so may become a
+    ///   compile-time error in the future.
+    ///
+    /// - Parameter hasher: The hasher to use when combining the components
+    ///   of this instance.
+    public func hash(into hasher: inout Hasher) {
+        hasher.combine(rawValue)
+    }
+
+}

+ 45 - 6
Sources/Binary/TLV.swift

@@ -18,7 +18,20 @@
 
 import Foundation
 
-public struct TLV<Type, Lenght> where Lenght: BinaryInteger {
+public protocol TLVProtocol {
+
+    associatedtype `Type`
+
+    associatedtype Lenght: BinaryInteger
+
+    associatedtype Value
+
+    var type: Type { get }
+
+    var value: Value { get set }
+}
+
+public struct TLV<Type, Lenght>: TLVProtocol where Lenght: BinaryInteger {
 
     public let type: Type
 
@@ -29,13 +42,12 @@ public struct TLV<Type, Lenght> where Lenght: BinaryInteger {
         self.value = value
     }
 
-    public func get<T>() throws -> T where T: Readable {
-        let stream = Input(bytes: value)
-        return try stream.read()
+    public func get<T>() throws -> T where T: BytesRepresentable {
+        return try T(value)
     }
 
-    public mutating func set<T>(_ value: T) throws where T: Writable {
-        self.value = withUnsafeBytes(of: value) { Bytes($0) }
+    public mutating func set<T>(_ value: T?) throws where T: BytesRepresentable {
+        self.value = value?.bytes ?? []
     }
 }
 
@@ -59,6 +71,33 @@ extension TLV: Writable where Type: Writable, Lenght: Writable {
 
 }
 
+extension Sequence where Element: TLVProtocol, Element.`Type`: Equatable, Element.Value == Bytes {
+
+    public func first<T>(valueOf type: Element.`Type`) throws -> T? where T: BytesRepresentable {
+        return try first(where: { $0.type == type }).map { try T($0.value) }
+    }
+
+    public func first<T>(where type: Element.`Type`, _ predicate: (T) throws -> Bool) throws -> Element? where T: BytesRepresentable {
+        return try first(where: { try predicate(try T($0.value)) })
+    }
+
+    public func sorted<T>(field: Element.`Type`, by areInIncreasingOrder: (T, T) throws -> Bool) throws -> [Self.Element] where T: BytesRepresentable {
+        return try sorted(by: { try areInIncreasingOrder(try T($0.value), try T($1.value)) })
+    }
+
+    public subscript<T>(_ type: Element.`Type`) -> T? where T: BytesRepresentable {
+        try? first(valueOf: type)
+    }
+}
+
+extension RangeReplaceableCollection where Element: TLVProtocol, Element.`Type`: Equatable {
+
+    public mutating func removeAll(_ type: Element.`Type`) {
+        removeAll(where: { $0.type == type })
+    }
+
+}
+
 extension TLV: CustomDebugStringConvertible {
 
     public var debugDescription: String {

+ 25 - 29
Sources/KDB/Database.swift

@@ -27,7 +27,7 @@ public class Database {
 
     let header: Header
 
-    public private(set) var root: Group!
+    public let root: Group
 
     public required init(from input: Input, compositeKey: CompositeKey) throws {
         header = try input.read()
@@ -52,7 +52,7 @@ public class Database {
         }
 
         let stream = Input(bytes: content)
-        self.root = try tree(from: stream)
+        self.root = try Root(from: stream, groups: Int(header.groups), entries: Int(header.entries))
     }
 
     public convenience init(from file: URL, compositeKey: CompositeKey) throws {
@@ -79,46 +79,42 @@ extension Database: Writable {
 
 }
 
-extension Database {
+private func Root(from input: Input, groups: Int, entries: Int) throws -> Group {
 
-    private func tree(from input: Input) throws -> Group {
+    let groups: [Group] = try input.read(maxLenght: groups)
+    let entries: [Entry] = try input.read(maxLenght: entries)
 
-        let groups: [Group] = try input.read(maxLenght: Int(header.groups))
-        let entries: [Entry] = try input.read(maxLenght: Int(header.entries))
+    let root = Group()
 
-        let root = Group()
+    for (i, group) in groups.enumerated() {
 
-        for (i, group) in groups.enumerated() {
+        guard let level1: UInt16 = group[.groupLevel] else { throw KDBError.corruptedDatabase }
 
-            guard let level1: UInt16 = group[.groupLevel] else { throw KDBError.corruptedDatabase }
-
-            if level1 == 0 {
-                root.childs.append(group)
-                continue
-            }
-
-            for (j, parent) in groups[0..<i].enumerated().reversed() {
-                guard let level2: UInt16 = parent[.groupLevel] else { throw KDBError.corruptedDatabase }
+        if level1 == 0 {
+            root.childs.append(group)
+            continue
+        }
 
-                if level2 < level1 {
-                    guard (level1 - level2) == 1 else { throw KDBError.corruptedDatabase }
-                    parent.childs.append(group)
-                    break
-                }
+        for (j, parent) in groups[0..<i].enumerated().reversed() {
+            guard let level2: UInt16 = parent[.groupLevel] else { throw KDBError.corruptedDatabase }
 
-                guard j > 0 else { throw KDBError.corruptedDatabase }
+            if level2 < level1 {
+                guard (level1 - level2) == 1 else { throw KDBError.corruptedDatabase }
+                parent.childs.append(group)
+                break
             }
 
+            guard j > 0 else { throw KDBError.corruptedDatabase }
         }
 
-        for entry in entries {
-            guard let groupID: UInt32 = entry[.groupID] else { throw KDBError.corruptedDatabase }
+    }
 
-            let group = try groups.first(where: .groupID, { $0 == groupID }) ?? root
-            group.entries.append(entry)
-        }
+    for entry in entries {
+        guard let groupID: UInt32 = entry[.groupID] else { throw KDBError.corruptedDatabase }
 
-        return root
+        let group = try groups.first(where: .groupID, { $0 == groupID }) ?? root
+        group.add(entry)
     }
 
+    return root
 }

+ 25 - 5
Sources/KDB/Entry.swift

@@ -41,9 +41,9 @@ let MetaEntryKeePassKitUserTemplates         = "KeePassKit User Templates"
 
 public final class Entry: Row, Streamable {
 
-    public static let End = Field.end
+    public static let End = Type.end
 
-    public enum Field: UInt16, Streamable {
+    public enum `Type`: UInt16, Streamable {
         case reserved           = 0x0000
         case uuid               = 0x0001
         case groupID            = 0x0002
@@ -62,12 +62,13 @@ public final class Entry: Row, Streamable {
         case end                = 0xFFFF
     }
 
-    public var fields: [TLV<Field, UInt32>]
+    var parent: Group?
+
+    public var fields: [Field<Type>]
 
     public required init() {
         fields = []
-    }
-    
+    }    
 }
 
 extension Entry {
@@ -75,4 +76,23 @@ extension Entry {
     var isMetaEntry: Bool {
         return false
     }
+
+    public func removeFromParent() {
+        parent?.entries.removeAll(where: { $0 == self })
+        fields.removeAll(.groupID)
+    }
+}
+
+extension Entry: Hashable {
+
+    public static func == (lhs: Entry, rhs: Entry) -> Bool {
+        guard let lhs = lhs[.uuid], let rhs = rhs[.uuid] else { return false }
+        return lhs == rhs
+    }
+
+    public func hash(into hasher: inout Hasher) {
+        if let uuid = self[.uuid] { hasher.combine(uuid) }
+        else if let title = self[.title] { hasher.combine(title) }
+    }
+
 }

+ 37 - 3
Sources/KDB/Group.swift

@@ -21,9 +21,9 @@ import Binary
 
 public final class Group: Row, Streamable {
 
-    public static let End = Field.end
+    public static let End = Type.end
 
-    public enum Field: UInt16, Streamable {
+    public enum `Type`: UInt16, Streamable {
         case reserved           = 0x0000
         case groupID            = 0x0001
         case name               = 0x0002
@@ -37,7 +37,9 @@ public final class Group: Row, Streamable {
         case end                = 0xFFFF
     }
 
-    public var fields: [TLV<Field, UInt32>]
+    var parent: Group?
+
+    public var fields: [Field<Type>]
 
     public var childs: [Group]
 
@@ -49,3 +51,35 @@ public final class Group: Row, Streamable {
         entries = []
     }
 }
+
+extension Group {
+
+    public func removeFromParent() {
+        parent?.childs.removeAll(where: { $0 == self })
+    }
+
+    public func add(_ entry: Entry) {
+        entry.removeFromParent()
+        entries.append(entry)
+        entry[.groupID] = self[.groupID]
+    }
+
+    public func add(_ group: Group) {
+        group.removeFromParent()
+        childs.append(group)
+    }
+}
+
+extension Group: Hashable {
+
+    public static func == (lhs: Group, rhs: Group) -> Bool {
+        guard let lhs = lhs[.groupLevel], let rhs = rhs[.groupLevel] else { return false }
+        return lhs == rhs
+    }
+
+    public func hash(into hasher: inout Hasher) {
+        if let groupLevel = self[.groupLevel] { hasher.combine(groupLevel) }
+        if let groupFlags = self[.groupFlags] { hasher.combine(groupFlags) }
+    }
+
+}

+ 29 - 18
Sources/KDB/Row.swift

@@ -19,34 +19,46 @@
 import Foundation
 import Binary
 
-public protocol Row {
-    associatedtype Field: Streamable, Equatable
+public typealias Field<Type> = TLV<Type, UInt32>
 
-    init()
+public protocol Row: class {
+
+    associatedtype `Type`: Streamable, Equatable
+
+    static var End: Type { get }
 
-    var fields: [TLV<Field, UInt32>] { get set }
+    var fields: [Field<Type>] { get set }
 
-    static var End: Field { get }
+    init()
 }
 
 extension Row {
 
-    public subscript (_ field: Field) -> Bytes? {
-        get { fields.first(where: { $0.type == field })?.value }
+    public subscript(_ type: Type) -> Bytes? {
+        get { fields.first(where: { $0.type == type })?.value }
         set {
-            fields.removeAll(where: { $0.type == field })
+            fields.removeAll(where: { $0.type == type })
             guard let value = newValue else { return }
-            let tlv = TLV<Field, UInt32>(type: field, value: value)
+            let tlv = Field(type: type, value: value)
             fields.insert(tlv, at: 0)
         }
     }
 
-    public subscript <T>(_ field: Field) -> T? where T: BytesRepresentable {
+    public func set(_ field: Field<Type>) {
+        fields.removeAll(where: { $0.type == field.type })
+        fields.insert(field, at: 0)
+    }
+
+    public subscript<T>(_ type: Type) -> T? where T: BytesRepresentable {
         get {
-            guard let bytes = self[field] else { return nil }
+            guard let bytes = self[type] else { return nil }
             return try? T(bytes)
         }
-        set { self[field] = newValue?.bytes }
+        set { self[type] = newValue?.bytes }
+    }
+
+    public func remove(_ type: Type) {
+        fields.removeAll(where: { $0.type == type })
     }
 }
 
@@ -55,7 +67,7 @@ extension Readable where Self: Row {
     public init(from input: Input) throws {
         self.init()
         while true {
-            let field: TLV<Field, UInt32> = try input.read()
+            let field = try input.read() as Field<Type>
             guard field.type != Self.End else { break }
             fields.append(field)
         }
@@ -67,7 +79,7 @@ extension Writable where Self: Row {
 
     public func write(to output: Output) throws {
         try output.write(fields)
-        let end = TLV<Field, UInt32>(type: Self.End, value: [])
+        let end = Field(type: Self.End, value: [])
         try output.write(end)
     }
     
@@ -75,19 +87,18 @@ extension Writable where Self: Row {
 
 extension Sequence where Element: Row {
 
-    public func first<T>(where field: Element.Field, _ predicate: (T) throws -> Bool) throws -> Element? where T: BytesRepresentable {
+    public func first<T>(where type: Element.`Type`, _ predicate: (T) throws -> Bool) throws -> Element? where T: BytesRepresentable {
         return try first(where: {
-            guard let bytes = $0[field] else { return false }
+            guard let bytes = $0[type] else { return false }
             return try predicate(try T(bytes))
         })
     }
 
-    public func sorted<T>(field: Element.Field, by areInIncreasingOrder: (T, T) throws -> Bool) throws -> [Self.Element] where T: BytesRepresentable {
+    public func sorted<T>(field: Element.`Type`, by areInIncreasingOrder: (T, T) throws -> Bool) throws -> [Self.Element] where T: BytesRepresentable {
         return try sorted(by: {
             guard let rhs = $0[field], let lhs = $1[field] else { return false }
             return try areInIncreasingOrder(try T(rhs), try T(lhs))
         })
     }
 
-    
 }

+ 70 - 0
Sources/KeePass/CompositeKey.swift

@@ -0,0 +1,70 @@
+// CompositeKey.swift
+// This file is part of KeePass.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePass is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePass is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePass. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+import Binary
+import Crypto
+import XML
+import KDB
+import KDBX
+
+public struct CompositeKey {
+
+    public static let KeyLength = 32
+
+    public let password: String
+
+    public let key: Bytes
+
+    public init(password: String) {
+        self.password = password
+        self.key = []
+    }
+
+    public init(password: String, key: Data) {
+        self.password = password
+
+        if let xml = try? XML.Document(xml: key), let base64: String = try? xml.KeyFile.Key.Data.get(), let key = Bytes(base64Encoded: base64) {
+            // KeePass 2 XML key file
+            self.key = key
+
+        } else if key.count == CompositeKey.KeyLength {
+            // Fixed 32 byte binary
+            self.key = Bytes(data: key)
+
+        } else if key.count == 2 * CompositeKey.KeyLength, let hex = String(data: key, encoding: .ascii), let key = Bytes(hex: hex) {
+            // Fixed 32 byte ASCII hex-encoded binary
+            self.key = key
+
+        } else {
+            // Arbitrary file
+            let binary = Bytes(data: key)
+            self.key = SHA256.hash(binary)
+        }
+    }
+
+    public init(password: String, key url: URL) throws {
+        let key = try Data(contentsOf: url)
+        self.init(password: password, key: key)
+    }
+
+}
+
+extension CompositeKey: KDB.CompositeKey { }
+
+extension CompositeKey: KDBX.CompositeKey { }

+ 29 - 0
Sources/KeePass/Database.swift

@@ -0,0 +1,29 @@
+// Database.swift
+// This file is part of KeePass.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePass is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePass is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePass. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+
+public protocol Database {
+
+    associatedtype Root: Group
+
+    typealias Entry = Root.Entries.Element
+
+    var root: Root { get }
+    
+}

+ 36 - 0
Sources/KeePass/Entry.swift

@@ -0,0 +1,36 @@
+// Entry.swift
+// This file is part of KeePass.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePass is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePass is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePass. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+
+public let EntryFieldTitle    = "Title"
+public let EntryFieldUserName = "UserName"
+public let EntryFieldPassword = "Password"
+public let EntryFieldURL      = "URL"
+public let EntryFieldNotes    = "Notes"
+
+public protocol Entry: RandomAccessCollection where Element == Field, Index == Int {
+    mutating func set(_ field: Element)
+}
+
+extension Entry {
+
+    subscript(_ field: String) -> Element? {
+        return first(where: { $0.name == field })
+    }
+}

+ 23 - 0
Sources/KeePass/Error.swift

@@ -0,0 +1,23 @@
+// Error.swift
+// This file is part of KeePassKit.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePassKit is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePassKit is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePassKit. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+
+public enum KeePassError: Error {
+    case invalidFileFormat
+}

+ 27 - 0
Sources/KeePass/Field.swift

@@ -0,0 +1,27 @@
+// Field.swift
+// This file is part of KeePass.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePass is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePass is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePass. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+import Binary
+
+public struct Field {
+    public var name: String
+    public var value: String?
+    public var isProtected: Bool
+    public var isReadeOnly: Bool
+}

+ 34 - 0
Sources/KeePass/Group.swift

@@ -0,0 +1,34 @@
+// Group.swift
+// This file is part of KeePass.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePass is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePass is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePass. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+
+public protocol Group {
+
+    associatedtype Entries: RandomAccessCollection where Entries.Element: Entry
+
+    associatedtype Groups: RandomAccessCollection where Groups.Element: Group
+    
+    var title: String { get set }
+
+    var icon: Int { get set }
+
+    var entries: Entries { get }
+
+    var groups: Groups { get }
+}

+ 126 - 0
Sources/KeePass/KDB.swift

@@ -0,0 +1,126 @@
+// KDB.swift
+// This file is part of KeePass.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePass is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePass is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePass. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+import Binary
+import KDB
+
+extension KDB.Database: Database {
+
+}
+
+extension KDB.Field where Type == KDB.Entry.`Type` {
+
+    init?(_ field: Field) {
+
+        let type: Type
+        let value = field.value?.bytes ?? []
+
+        switch field.name {
+        case EntryFieldTitle:
+            type = .title
+        case EntryFieldURL:
+            type = .url
+        case EntryFieldUserName:
+            type = .username
+        case EntryFieldPassword:
+            type = .password
+        case EntryFieldNotes:
+            type = .notes
+        default:
+            return nil
+        }
+
+        self.init(type: type, value: value)
+    }
+}
+
+extension KDB.Group: Group {
+
+    public var title: String {
+        get { self[.name] ?? "" }
+        set { self[.name] = newValue }
+    }
+
+    public var icon: Int {
+        get { self[.iconID] ?? 0 }
+        set { self[.iconID] = newValue }
+    }
+
+    public var groups: [KDB.Group] { childs }
+}
+
+extension KDB.Entry: Entry {
+
+    public subscript(position: Int) -> Field {
+        Field( fields[position] )
+    }
+
+    public var startIndex: Int {
+        fields.startIndex
+    }
+
+    public var endIndex: Int {
+        fields.endIndex
+    }
+
+    public func index(after i: Int) -> Int {
+        fields.index(after: i)
+    }
+
+    public func set(_ field: Field) {
+        guard let field = KDB.Field(field) else { return }
+        set(field)
+    }
+
+}
+
+extension Field {
+
+    init(_ field: KDB.Field<KDB.Entry.`Type`>) {
+
+        switch field.type {
+        case .title:
+            name = EntryFieldTitle
+            isReadeOnly = false
+            isProtected = false
+        case .url:
+            name = EntryFieldURL
+            isReadeOnly = false
+            isProtected = false
+        case .username:
+            name = EntryFieldUserName
+            isReadeOnly = false
+            isProtected = false
+        case .password:
+            name = EntryFieldPassword
+            isReadeOnly = false
+            isProtected = true
+        case .notes:
+            name = EntryFieldNotes
+            isReadeOnly = false
+            isProtected = false
+        default:
+            name = ""
+            isReadeOnly = true
+            isProtected = false
+        }
+
+        value = try? field.get()
+    }
+}

+ 98 - 0
Sources/KeePass/KDBX.swift

@@ -0,0 +1,98 @@
+// KDBX.swift
+// This file is part of KeePass.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePass is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePass is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePass. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+import Binary
+import XML
+import KDBX
+
+extension KDBX.File: Database {
+    public var root: Element { database.document.root }
+}
+
+extension Element {
+    private var this: XML.Element { self as XML.Element }
+}
+
+extension XML.Element {
+
+    convenience init(_ field: Field) {
+        self.init(name: field.name, value: field.value, attributes: [:])
+    }
+
+}
+
+extension Field {
+
+    init(_ element: XML.Element) {
+        name = element.name
+        value = element.value
+        isProtected = false
+        isReadeOnly = false
+    }
+}
+
+extension XML.Element: Entry {
+
+    public func set(_ field: Field) {
+        allDescendants(where: { $0.name == field.name })
+            .forEach { $0.removeFromParent() }
+        addChild( XML.Element(field) )
+    }
+
+    public subscript(position: Int) -> Field {
+        Field( children[position] )
+    }
+
+    public func index(after i: Int) -> Int {
+        children.index(after: i)
+    }
+
+    public var startIndex: Int {
+        children.startIndex
+    }
+
+    public var endIndex: Int {
+        children.endIndex
+    }
+}
+
+extension XML.Element: Group {
+
+    public var title: String {
+        get { attributes["Title"] ?? "" }
+        set { attributes["Title"] = newValue }
+    }
+
+    public var icon: Int {
+        get {
+            guard let attr = attributes["Icon"], let icon = Int(attr) else { return 0 }
+            return icon
+        }
+        set { attributes["Icon"] = "\(newValue)" }
+    }
+
+    public var entries: [Element] {
+        allDescendants(where: { $0.name == "Entry"})
+    }
+
+    public var groups: [Element] {
+        allDescendants(where: { $0.name == "Group"})
+    }
+
+}

+ 52 - 2
Sources/KeePass/KeePass.swift

@@ -1,3 +1,53 @@
-struct KeePass {
-    var text = "Hello, World!"
+// KeePass.swift
+// This file is part of KeePassKit.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePassKit is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePassKit is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePassKit. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+import Binary
+import KDB
+import KDBX
+
+public let FileSignature: UInt32 = 0x9AA2D903
+
+public enum FileFormat: UInt32, Streamable {
+    case kdb        = 0xB54BFB65
+    case prekdbx    = 0xB54BFB66
+    case kdbx       = 0xB54BFB67
+}
+
+public class KeePass {
+
+    public static func open(contentOf url: URL, compositeKey: CompositeKey) throws -> AnyDatabase {
+
+        let bytes = try Bytes(contentsOf: url)
+        let stream = Input(bytes: bytes)
+
+        guard try stream.read() == FileSignature else {
+            throw KeePassError.invalidFileFormat
+        }
+
+        let format = try stream.read() as FileFormat
+
+        switch format {
+        case .kdb:
+            return AnyDatabase( try KDB.Database(from: stream, compositeKey: compositeKey) )
+        case .prekdbx, .kdbx:
+            return AnyDatabase ( try KDBX.File(from: stream, compositeKey: compositeKey) )
+        }
+    }
+
 }

+ 187 - 0
Sources/KeePass/TypeErasure.swift

@@ -0,0 +1,187 @@
+// TypeErasure.swift
+// This file is part of KeePass.
+//
+// Copyright © 2019 Maxime Epain. All rights reserved.
+//
+// KeePass is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// KeePass is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with KeePass. If not, see <https://www.gnu.org/licenses/>.
+
+import Foundation
+
+@inline(never)
+internal func _abstract(file: StaticString = #file, line: UInt = #line) -> Never {
+    fatalError("Method must be overridden", file: file, line: line)
+}
+
+// MARK: - Database
+
+internal class _AnyDatabaseBoxBase: Database {
+    internal var root: AnyGroup { _abstract() }
+}
+
+internal final class _AnyDatabaseBox<Base>: _AnyDatabaseBoxBase where Base: Database {
+    internal override var root: AnyGroup { AnyGroup( _base.root ) }
+    internal var _base: Base
+    internal init(_ base: Base) {
+        self._base = base
+    }
+}
+
+public class AnyDatabase: Database {
+
+    public var root: AnyGroup { _box.root }
+
+    internal let _box: _AnyDatabaseBoxBase
+    internal init<T>(_ base: T) where T: Database {
+        _box = _AnyDatabaseBox(base)
+    }
+    
+}
+
+// MARK: - Group
+
+internal class _AnyGroupBoxBase: Group {
+
+    internal var title: String {
+        get { _abstract() }
+        set { _abstract() }
+    }
+
+    internal var icon: Int {
+        get { _abstract() }
+        set { _abstract() }
+    }
+
+    internal var entries: AnyRandomAccessCollection<AnyEntry> { _abstract() }
+    internal var groups: AnyRandomAccessCollection<AnyGroup> { _abstract() }
+}
+
+internal final class _AnyGroupBox<Base>: _AnyGroupBoxBase where Base: Group {
+
+    internal override var title: String {
+        get { _base.title }
+        set { _base.title = newValue }
+    }
+
+    internal override var icon: Int {
+        get { _base.icon }
+        set { _base.icon = newValue }
+    }
+
+    internal override var entries: AnyRandomAccessCollection<AnyEntry> { AnyRandomAccessCollection<AnyEntry>(_base.entries.map { AnyEntry($0) }) }
+    internal override var groups: AnyRandomAccessCollection<AnyGroup> { AnyRandomAccessCollection<AnyGroup>(_base.groups.map { AnyGroup($0) }) }
+
+    internal var _base: Base
+    internal init(_ base: Base) {
+        self._base = base
+    }
+}
+
+public class AnyGroup: Group {
+
+    public var title: String {
+        get { _box.title }
+        set { _box.title = newValue }
+    }
+
+    public var icon: Int {
+        get { _box.icon }
+        set { _box.icon = newValue }
+    }
+
+    public var entries: AnyRandomAccessCollection<AnyEntry> { _box.entries }
+    public var groups: AnyRandomAccessCollection<AnyGroup> { _box.groups }
+
+    internal let _box: _AnyGroupBoxBase
+    internal init<T>(_ base: T) where T: Group {
+        _box = _AnyGroupBox(base)
+    }
+
+}
+
+// MARK: - Entry
+
+internal class _AnyEntryBoxBase: Entry {
+    internal subscript(position: Int) -> Field { _abstract() }
+    internal var startIndex: Int { _abstract() }
+    internal var endIndex: Int { _abstract() }
+    internal func index(after i: Int) -> Int { _abstract() }
+    internal func index(before i: Int) -> Int { _abstract() }
+    internal func set(_ field: Field) { _abstract() }
+}
+
+internal final class _AnyEntryBox<Base>: _AnyEntryBoxBase where Base: Entry {
+
+    internal override subscript(position: Int) -> Element {
+        _base[position]
+    }
+
+    internal override var startIndex: Int {
+        _base.startIndex
+    }
+
+    internal override var endIndex: Int {
+        _base.endIndex
+    }
+
+    internal override func index(after i: Int) -> Int {
+        _base.index(after: i)
+    }
+
+    internal override func index(before i: Int) -> Int {
+        _base.index(before: i)
+    }
+
+    internal override func set(_ field: Element) {
+        _base.set(field)
+    }
+
+    internal var _base: Base
+    internal init(_ base: Base) {
+        self._base = base
+    }
+
+}
+
+public final class AnyEntry: Entry {
+
+    public subscript(position: Int) -> Field {
+        _box[position]
+    }
+
+    public var startIndex: Int {
+        _box.startIndex
+    }
+
+    public var endIndex: Int {
+        _box.endIndex
+    }
+
+    public func index(after i: Int) -> Int {
+        _box.index(after: i)
+    }
+
+    public func index(before i: Int) -> Int {
+        _box.index(before: i)
+    }
+
+    public func set(_ field: Field) {
+        _box.set(field)
+    }
+
+    internal let _box: _AnyEntryBoxBase
+    internal init<T>(_ base: T) where T: Entry {
+        _box = _AnyEntryBox(base)
+    }
+
+}