Quellcode durchsuchen

Add hight-level KeePass protocols and type-erase

maxep vor 6 Jahren
Ursprung
Commit
50667925da

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