feat: copper accent theme, always-expanded group filter, app icon; rename MyPass to KeeVault

- Custom copper AccentColor and warm VaultBackground color assets (light/dark),
  applied to both the main app and the AutoFill extension (which needed its own
  copy of the asset catalog plus ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME
  and an explicit .tint() modifier, since extensions don't pick up the app-wide
  accent color the same way a full SwiftUI App/Scene does).
- GroupFilterView no longer uses DisclosureGroup for subgroups -- the whole tree
  renders always-expanded (indented per depth) so active filters are visible at
  a glance instead of requiring tap-to-expand.
- Added a placeholder app icon (padlock glyph, light/dark/tinted variants plus
  macOS sizes) -- previously the icon slot existed but had no actual images,
  which blocks App Store/TestFlight archive validation.
- Added ITSAppUsesNonExemptEncryption = NO (standard encryption only, via
  KeePassKit) to skip the export-compliance prompt on each upload.
- Renamed the app from MyPass to KeeVault throughout: Xcode project/targets/
  schemes, the MyPassCore package (now KeeVaultCore) and every import site,
  folder and file names, bundle identifiers (org.antiloop222.keevault) and
  their App Group/Keychain-group entitlements, and remaining UI/string
  references -- MyPass was already taken as an App Store app name.
This commit is contained in:
2026-09-20 15:05:19 +02:00
parent a8d612f745
commit 8bba64076f
74 changed files with 363 additions and 176 deletions
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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 }
}
@@ -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 }
}
@@ -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
}
}
@@ -0,0 +1,32 @@
import Foundation
public struct ProtectedString: Equatable {
private let obfuscated: [UInt8]
private let key: [UInt8]
public let isProtected: Bool
public init(_ value: String, isProtected: Bool = true) {
self.isProtected = isProtected
let bytes = Array(value.utf8)
let k = (0 ..< bytes.count).map { _ in UInt8.random(in: 0 ... 255) }
self.key = k
self.obfuscated = zip(bytes, k).map { $0 ^ $1 }
}
public func reveal() -> String {
let bytes = zip(obfuscated, key).map { $0 ^ $1 }
return String(bytes: bytes, encoding: .utf8) ?? ""
}
public static func == (lhs: ProtectedString, rhs: ProtectedString) -> Bool {
lhs.reveal() == rhs.reveal() && lhs.isProtected == rhs.isProtected
}
}
extension ProtectedString: CustomStringConvertible {
public var description: String { isProtected ? "***" : reveal() }
}
extension ProtectedString: CustomDebugStringConvertible {
public var debugDescription: String { "ProtectedString(isProtected: \(isProtected))" }
}
@@ -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)
}
}