From 45d74416363ddbe5d7cb5e05d9070f83c17e02b7 Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Fri, 22 May 2026 14:03:13 +0200 Subject: [PATCH] feat: add core KDBX data models --- .../MyPassCore/Models/Attachment.swift | 13 +++++ .../MyPassCore/Models/CustomField.swift | 13 +++++ .../MyPassCore/Models/DatabaseMetadata.swift | 10 ++++ .../Sources/MyPassCore/Models/Entry.swift | 57 +++++++++++++++++++ .../Sources/MyPassCore/Models/Group.swift | 27 +++++++++ .../MyPassCore/Models/KDBXDatabase.swift | 11 ++++ .../MyPassCore/Models/TOTPConfig.swift | 45 +++++++++++++++ 7 files changed, 176 insertions(+) create mode 100644 MyPassCore/Sources/MyPassCore/Models/Attachment.swift create mode 100644 MyPassCore/Sources/MyPassCore/Models/CustomField.swift create mode 100644 MyPassCore/Sources/MyPassCore/Models/DatabaseMetadata.swift create mode 100644 MyPassCore/Sources/MyPassCore/Models/Entry.swift create mode 100644 MyPassCore/Sources/MyPassCore/Models/Group.swift create mode 100644 MyPassCore/Sources/MyPassCore/Models/KDBXDatabase.swift create mode 100644 MyPassCore/Sources/MyPassCore/Models/TOTPConfig.swift diff --git a/MyPassCore/Sources/MyPassCore/Models/Attachment.swift b/MyPassCore/Sources/MyPassCore/Models/Attachment.swift new file mode 100644 index 0000000..2b75412 --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Models/Attachment.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct Attachment: Identifiable, Equatable { + public var id: UUID + public var name: String + public var data: Data + + public init(id: UUID = UUID(), name: String, data: Data) { + self.id = id + self.name = name + self.data = data + } +} diff --git a/MyPassCore/Sources/MyPassCore/Models/CustomField.swift b/MyPassCore/Sources/MyPassCore/Models/CustomField.swift new file mode 100644 index 0000000..daf72a1 --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Models/CustomField.swift @@ -0,0 +1,13 @@ +import Foundation + +public struct CustomField: Identifiable, Equatable { + public var id: UUID + public var key: String + public var value: ProtectedString + + public init(id: UUID = UUID(), key: String, value: ProtectedString) { + self.id = id + self.key = key + self.value = value + } +} diff --git a/MyPassCore/Sources/MyPassCore/Models/DatabaseMetadata.swift b/MyPassCore/Sources/MyPassCore/Models/DatabaseMetadata.swift new file mode 100644 index 0000000..73f4a76 --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Models/DatabaseMetadata.swift @@ -0,0 +1,10 @@ +import Foundation + +public struct DatabaseMetadata: Equatable { + public var name: String + public var description: String + public init(name: String, description: String = "") { + self.name = name + self.description = description + } +} diff --git a/MyPassCore/Sources/MyPassCore/Models/Entry.swift b/MyPassCore/Sources/MyPassCore/Models/Entry.swift new file mode 100644 index 0000000..3f62cc9 --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Models/Entry.swift @@ -0,0 +1,57 @@ +import Foundation + +public struct Entry: Identifiable, Equatable, Hashable { + public var id: UUID + public var title: String + public var username: String + public var password: ProtectedString + public var url: String + public var notes: String + public var customFields: [CustomField] + public var attachments: [Attachment] + public var totp: TOTPConfig? + public var tags: [String] + public var iconIndex: Int + public var expiryDate: Date? + public var creationDate: Date + public var modificationDate: Date + public var history: [Entry] + + public init( + id: UUID = UUID(), + title: String = "", + username: String = "", + password: ProtectedString = ProtectedString("", isProtected: true), + url: String = "", + notes: String = "", + customFields: [CustomField] = [], + attachments: [Attachment] = [], + totp: TOTPConfig? = nil, + tags: [String] = [], + iconIndex: Int = 0, + expiryDate: Date? = nil, + creationDate: Date = Date(), + modificationDate: Date = Date(), + history: [Entry] = [] + ) { + self.id = id + self.title = title + self.username = username + self.password = password + self.url = url + self.notes = notes + self.customFields = customFields + self.attachments = attachments + self.totp = totp + self.tags = tags + self.iconIndex = iconIndex + self.expiryDate = expiryDate + self.creationDate = creationDate + self.modificationDate = modificationDate + self.history = history + } + + // Hashable by ID so SwiftUI List(selection:) works without requiring all fields to be Hashable. + public func hash(into hasher: inout Hasher) { hasher.combine(id) } + public static func == (lhs: Entry, rhs: Entry) -> Bool { lhs.id == rhs.id } +} diff --git a/MyPassCore/Sources/MyPassCore/Models/Group.swift b/MyPassCore/Sources/MyPassCore/Models/Group.swift new file mode 100644 index 0000000..f87bb8c --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Models/Group.swift @@ -0,0 +1,27 @@ +import Foundation + +public struct Group: Identifiable, Equatable, Hashable { + public var id: UUID + public var name: String + public var iconIndex: Int + public var subgroups: [Group] + public var entries: [Entry] + + public init( + id: UUID = UUID(), + name: String, + iconIndex: Int = 0, + subgroups: [Group] = [], + entries: [Entry] = [] + ) { + self.id = id + self.name = name + self.iconIndex = iconIndex + self.subgroups = subgroups + self.entries = entries + } + + // Hashable by ID so SwiftUI List(selection:) works without hashing the full tree. + public func hash(into hasher: inout Hasher) { hasher.combine(id) } + public static func == (lhs: Group, rhs: Group) -> Bool { lhs.id == rhs.id } +} diff --git a/MyPassCore/Sources/MyPassCore/Models/KDBXDatabase.swift b/MyPassCore/Sources/MyPassCore/Models/KDBXDatabase.swift new file mode 100644 index 0000000..45be888 --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Models/KDBXDatabase.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct KDBXDatabase: Equatable { + public var metadata: DatabaseMetadata + public var root: Group + + public init(metadata: DatabaseMetadata, root: Group) { + self.metadata = metadata + self.root = root + } +} diff --git a/MyPassCore/Sources/MyPassCore/Models/TOTPConfig.swift b/MyPassCore/Sources/MyPassCore/Models/TOTPConfig.swift new file mode 100644 index 0000000..5db330a --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Models/TOTPConfig.swift @@ -0,0 +1,45 @@ +import Foundation + +public enum TOTPAlgorithm: String, Equatable, CaseIterable { + case sha1 = "SHA1" + case sha256 = "SHA256" + case sha512 = "SHA512" +} + +public struct TOTPConfig: Equatable { + public var secret: String + public var period: Int + public var digits: Int + public var algorithm: TOTPAlgorithm + + public init(secret: String, period: Int = 30, digits: Int = 6, algorithm: TOTPAlgorithm = .sha1) { + self.secret = secret + self.period = period + self.digits = digits + self.algorithm = algorithm + } + + /// Parses an otpauth://totp/ URI (standard KeePass TOTP storage format). + public static func parse(from uri: String) -> TOTPConfig? { + guard let url = URL(string: uri), + url.scheme == "otpauth", + url.host == "totp", + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + else { return nil } + let params = Dictionary( + uniqueKeysWithValues: (components.queryItems ?? []).compactMap { item in + item.value.map { (item.name, $0) } + } + ) + guard let secret = params["secret"] else { return nil } + let period = Int(params["period"] ?? "30") ?? 30 + let digits = Int(params["digits"] ?? "6") ?? 6 + let algo: TOTPAlgorithm + switch params["algorithm"]?.uppercased() { + case "SHA256": algo = .sha256 + case "SHA512": algo = .sha512 + default: algo = .sha1 + } + return TOTPConfig(secret: secret, period: period, digits: digits, algorithm: algo) + } +}