Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion ios/NimShare/Sources/ApiTokensView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,13 @@ struct ApiTokensView: View {
}
Section {
Button {
UIPasteboard.general.string = c.rawToken
// v1.11.82 (Security-Review): API-Token ist ein Secret — nur lokal
// in die Zwischenablage (kein Universal-Clipboard-Sync auf andere
// Geräte) und nach 60 s automatisch löschen.
UIPasteboard.general.setItems(
[["public.utf8-plain-text": c.rawToken]],
options: [.localOnly: true,
.expirationDate: Date().addingTimeInterval(60)])
} label: { Label("Kopieren", systemImage: "doc.on.doc") }
}
}
Expand Down
11 changes: 10 additions & 1 deletion ios/NimShare/Sources/KeyStoreLicensesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,16 @@ struct KeyStoreLicensesView: View {
KeyStoreLicenseAddSheet { Task { await load() } }
}
.alert("Lizenzschlüssel", isPresented: Binding(get: { revealedValue != nil }, set: { if !$0 { revealedValue = nil } })) {
Button("Kopieren") { UIPasteboard.general.string = revealedValue }
Button("Kopieren") {
// v1.11.82 (Security-Review): Lizenzschlüssel ist ein Secret — lokal-only
// in die Zwischenablage und nach 60 s automatisch löschen.
if let v = revealedValue {
UIPasteboard.general.setItems(
[["public.utf8-plain-text": v]],
options: [.localOnly: true,
.expirationDate: Date().addingTimeInterval(60)])
}
}
Button("Schließen", role: .cancel) { revealedValue = nil }
} message: { Text(revealedValue ?? "") }
.alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
Expand Down
11 changes: 10 additions & 1 deletion ios/NimShare/Sources/KeyStoreView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,16 @@ struct KeyStoreView: View {
KeyStoreEntrySheet(existing: e) { Task { await load() } }
}
.alert("Lizenzschlüssel", isPresented: Binding(get: { revealedValue != nil }, set: { if !$0 { revealedValue = nil } })) {
Button("Kopieren") { UIPasteboard.general.string = revealedValue }
Button("Kopieren") {
// v1.11.82 (Security-Review): Lizenzschlüssel ist ein Secret — lokal-only
// in die Zwischenablage und nach 60 s automatisch löschen.
if let v = revealedValue {
UIPasteboard.general.setItems(
[["public.utf8-plain-text": v]],
options: [.localOnly: true,
.expirationDate: Date().addingTimeInterval(60)])
}
}
Button("Schließen", role: .cancel) { revealedValue = nil }
} message: { Text(revealedValue ?? "") }
.alert("Fehler", isPresented: Binding(get: { error != nil }, set: { if !$0 { error = nil } })) {
Expand Down
7 changes: 6 additions & 1 deletion ios/NimShare/Sources/Keychain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ enum Keychain {
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
// v1.11.82 (Security-Review): WhenUnlocked statt AfterFirstUnlock — das
// Voll-Session-Token darf nur bei entsperrtem Gerät entschlüsselbar sein,
// nicht dauerhaft nach dem ersten Unlock seit Boot. ThisDeviceOnly bleibt
// (kein iCloud-Sync, kein Backup-Leak). Die App liest den Token ohnehin nur
// im Vordergrund (Login/Bootstrap/Request-Header), kein Background-Bedarf.
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
]
return SecItemAdd(q as CFDictionary, nil) == errSecSuccess
}
Expand Down
9 changes: 6 additions & 3 deletions ios/NimShare/Sources/ServerConfigView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,16 @@ struct ServerConfigView: View {
private var isValid: Bool {
let trimmed = input.trimmingCharacters(in: .whitespaces)
guard let u = URL(string: trimmed), let s = u.scheme else { return false }
return (s == "http" || s == "https") && u.host != nil
// v1.11.82 (Security-Review): nur HTTPS. Cleartext-HTTP würde das Bearer-Token
// im Klartext übertragen; ATS blockt es ohnehin zur Laufzeit, hier lehnen wir es
// schon in der Eingabe ab (klare Fehlermeldung statt stiller Verbindungsfehler).
return s.lowercased() == "https" && u.host != nil
}

private func save() {
let trimmed = input.trimmingCharacters(in: .whitespaces)
guard let u = URL(string: trimmed) else {
error = "Ungültige URL"
guard let u = URL(string: trimmed), u.scheme?.lowercased() == "https", u.host != nil else {
error = "Bitte eine gültige HTTPS-URL angeben (http:// wird aus Sicherheitsgründen nicht unterstützt)."
return
}
auth.setServer(u)
Expand Down
18 changes: 17 additions & 1 deletion ios/NimShare/Sources/TmpFile.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,23 @@ enum TmpFile {
// Verzeichnis vorbereiten — wir ignorieren „exists"-Fehler; das
// UUID ist neu, kann eigentlich nicht kollidieren.
try? FileManager.default.createDirectory(at: base, withIntermediateDirectories: true)
return base.appendingPathComponent(filename.isEmpty ? "file" : filename)
// v1.11.82 (Security-Review): Datenschutzklasse für (potenziell sensible) Downloads
// — lesbar solange offen (QuickLook/AVPlayer), sonst bei gesperrtem Gerät geschützt.
// Best-effort; das tmp-Verzeichnis wird von iOS ohnehin nicht ins Backup übernommen.
try? FileManager.default.setAttributes(
[.protectionKey: FileProtectionType.completeUnlessOpen], ofItemAtPath: base.path)
return base.appendingPathComponent(Self.sanitizedFilename(filename))
}

/// v1.11.82 (Security-Review): server-gelieferter Dateiname darf keine Pfad-Separatoren
/// oder „..“ einschleusen. Nur der letzte Pfad-Bestandteil, Separatoren entschärft.
/// (Der Schreibpfad bleibt so oder so im App-Sandbox-tmp, das ist Defense-in-Depth.)
private static func sanitizedFilename(_ name: String) -> String {
var n = (name as NSString).lastPathComponent
n = n.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "\\", with: "_")
n = n.trimmingCharacters(in: CharacterSet(charactersIn: " ."))
return n.isEmpty ? "file" : n
}

/// iPad-safe Share-Sheet. Auf iPhone verhält es sich wie üblich modal,
Expand Down
Loading