Skip to content

Commit f0a250b

Browse files
feat: track server certificate trust status (#4222)
* feat: track server certificate trust status Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com> * fix: download videos before playback with trusted certificates Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com> * chore: bump project build number to 2 Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com> --------- Signed-off-by: Marino Faggiana <marino.faggiana@nextcloud.com>
1 parent d307ab5 commit f0a250b

6 files changed

Lines changed: 298 additions & 35 deletions

File tree

‎Nextcloud.xcodeproj/project.pbxproj‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6307,7 +6307,7 @@
63076307
CLANG_WARN_UNREACHABLE_CODE = YES;
63086308
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
63096309
COPY_PHASE_STRIP = NO;
6310-
CURRENT_PROJECT_VERSION = 1;
6310+
CURRENT_PROJECT_VERSION = 2;
63116311
DEAD_CODE_STRIPPING = YES;
63126312
DEBUG_INFORMATION_FORMAT = dwarf;
63136313
DEVELOPMENT_TEAM = NKUJUXUJ3B;
@@ -6375,7 +6375,7 @@
63756375
CLANG_WARN_UNREACHABLE_CODE = YES;
63766376
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
63776377
COPY_PHASE_STRIP = NO;
6378-
CURRENT_PROJECT_VERSION = 1;
6378+
CURRENT_PROJECT_VERSION = 2;
63796379
DEAD_CODE_STRIPPING = YES;
63806380
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
63816381
DEVELOPMENT_TEAM = NKUJUXUJ3B;

‎iOSClient/Networking/NCNetworking.swift‎

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ protocol NCTransferDelegate: AnyObject {
3636
serverUrl: String)
3737
}
3838

39+
enum NCServerCertificateTrustStatus: Sendable {
40+
case systemTrusted
41+
case userTrusted
42+
case untrusted
43+
}
44+
3945
class NCNetworking: @unchecked Sendable, NextcloudKitDelegate {
4046
static let shared = NCNetworking()
4147

@@ -58,6 +64,12 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate {
5864
let backgroundSession = NKBackground(nkCommonInstance: NextcloudKit.shared.nkCommonInstance)
5965
let nkComm = NextcloudKit.shared.nkCommonInstance
6066

67+
private let certificateTrustStatusQueue = DispatchQueue(
68+
label: "com.nextcloud.networking.certificate-trust-status",
69+
attributes: .concurrent
70+
)
71+
private var certificateTrustStatuses: [String: NCServerCertificateTrustStatus] = [:]
72+
6173
var lastReachability: Bool = true
6274
var networkReachability: NKTypeReachability?
6375
weak var certificateDelegate: ClientCertificateDelegate?
@@ -138,6 +150,40 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate {
138150

139151
func request<Value>(_ request: DataRequest, didParseResponse response: AFDataResponse<Value>) { }
140152

153+
func certificateTrustStatus(for host: String) -> NCServerCertificateTrustStatus? {
154+
guard !host.isEmpty else {
155+
return nil
156+
}
157+
158+
return certificateTrustStatusQueue.sync {
159+
certificateTrustStatuses[host.lowercased()]
160+
}
161+
}
162+
163+
func requiresUserTrustedCertificate(for host: String) -> Bool {
164+
certificateTrustStatus(for: host) == .userTrusted
165+
}
166+
167+
func resetCertificateTrustStatus(for host: String) {
168+
guard !host.isEmpty else {
169+
return
170+
}
171+
172+
_ = certificateTrustStatusQueue.sync(flags: .barrier) {
173+
certificateTrustStatuses.removeValue(forKey: host.lowercased())
174+
}
175+
}
176+
177+
private func setCertificateTrustStatus(_ status: NCServerCertificateTrustStatus, for host: String) {
178+
guard !host.isEmpty else {
179+
return
180+
}
181+
182+
certificateTrustStatusQueue.async(flags: .barrier) {
183+
self.certificateTrustStatuses[host.lowercased()] = status
184+
}
185+
}
186+
141187
// MARK: - Pinning check
142188

143189
public func checkTrustedChallenge(_ session: URLSession,
@@ -154,13 +200,20 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate {
154200
}
155201
#else
156202
let protectionSpace = challenge.protectionSpace
203+
204+
guard protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else {
205+
completionHandler(.performDefaultHandling, nil)
206+
return
207+
}
208+
157209
let directoryCertificate = utilityFileSystem.directoryCertificates
158210
let host = protectionSpace.host
159211
let certificateSavedPath = (directoryCertificate as NSString).appendingPathComponent("\(host).der")
160212

161213
guard let trust = protectionSpace.serverTrust,
162214
let certificates = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
163215
let certificate = certificates.first else {
216+
setCertificateTrustStatus(.untrusted, for: host)
164217
completionHandler(.performDefaultHandling, nil)
165218
return
166219
}
@@ -177,19 +230,25 @@ class NCNetworking: @unchecked Sendable, NextcloudKitDelegate {
177230
let tmpPath = (directoryCertificate as NSString).appendingPathComponent("\(host).tmp")
178231
try? certificateData.write(to: URL(fileURLWithPath: tmpPath), options: .atomic)
179232

180-
var isTrusted = false
233+
let trustStatus: NCServerCertificateTrustStatus
181234

182235
if isServerTrusted {
183-
isTrusted = true
236+
trustStatus = .systemTrusted
184237
} else if let savedData = try? Data(contentsOf: URL(fileURLWithPath: certificateSavedPath)),
185238
savedData == certificateData {
186-
isTrusted = true
239+
trustStatus = .userTrusted
240+
} else {
241+
trustStatus = .untrusted
187242
}
188243

244+
self.setCertificateTrustStatus(trustStatus, for: host)
245+
189246
DispatchQueue.main.async {
190-
if isTrusted {
247+
switch trustStatus {
248+
case .systemTrusted, .userTrusted:
191249
completionHandler(.useCredential, URLCredential(trust: trust))
192-
} else {
250+
251+
case .untrusted:
193252
(UIApplication.shared.delegate as? AppDelegate)?.trustCertificateError(host: host)
194253
completionHandler(.performDefaultHandling, nil)
195254
}

‎iOSClient/Viewer/NCViewerMedia/Content/Video/NCVideoPlaybackCoverView.swift‎

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ struct NCVideoPlaybackCoverView: View {
1010
let isPlayEnabled: Bool
1111
let isLoading: Bool
1212
let isLaunchingPlayback: Bool
13+
let statusMessage: String?
14+
let onCancel: (() -> Void)?
1315
let onToggleChrome: (() -> Void)?
1416
let onPlay: () -> Void
1517

@@ -44,35 +46,62 @@ struct NCVideoPlaybackCoverView: View {
4446
onToggleChrome?()
4547
}
4648

47-
Button {
48-
guard isPlayEnabled else {
49-
return
49+
VStack(spacing: 12) {
50+
Button {
51+
guard isPlayEnabled else {
52+
return
53+
}
54+
55+
onPlay()
56+
} label: {
57+
ZStack {
58+
if isLoading || isLaunchingPlayback {
59+
ProgressView()
60+
.controlSize(.large)
61+
.tint(.white)
62+
.transition(.opacity)
63+
} else {
64+
Image(systemName: "play.fill")
65+
.font(.system(size: 36, weight: .regular))
66+
.foregroundStyle(isPlayEnabled ? .white : .black.opacity(0.35))
67+
.videoControlIconShadow()
68+
.transition(.opacity)
69+
}
70+
}
71+
.frame(width: 62, height: 62)
72+
.coverPlayButtonBackground(isEnabled: isPlayEnabled)
5073
}
74+
.disabled(!isPlayEnabled || isLoading || isLaunchingPlayback)
75+
.scaleEffect(isLaunchingPlayback ? 1.06 : 1)
76+
.animation(.easeInOut(duration: 0.18), value: isLoading)
77+
.animation(.easeInOut(duration: 0.14), value: isLaunchingPlayback)
78+
.accessibilityLabel(Text(NSLocalizedString("_play_", comment: "")))
79+
80+
if let statusMessage {
81+
VStack(spacing: 10) {
82+
Text(statusMessage)
83+
.font(.subheadline.weight(.semibold))
84+
.foregroundStyle(.white)
85+
.multilineTextAlignment(.center)
5186

52-
onPlay()
53-
} label: {
54-
ZStack {
55-
if isLoading || isLaunchingPlayback {
56-
ProgressView()
57-
.controlSize(.large)
87+
if let onCancel {
88+
Button(NSLocalizedString("_cancel_", comment: "")) {
89+
onCancel()
90+
}
91+
.font(.subheadline.weight(.semibold))
92+
.buttonStyle(.borderedProminent)
5893
.tint(.white)
59-
.transition(.opacity)
60-
} else {
61-
Image(systemName: "play.fill")
62-
.font(.system(size: 36, weight: .regular))
63-
.foregroundStyle(isPlayEnabled ? .white : .black.opacity(0.35))
64-
.videoControlIconShadow()
65-
.transition(.opacity)
94+
.foregroundStyle(.black)
95+
}
6696
}
97+
.padding(.horizontal, 14)
98+
.padding(.vertical, 10)
99+
.background(
100+
.black.opacity(0.46),
101+
in: RoundedRectangle(cornerRadius: 10, style: .continuous)
102+
)
67103
}
68-
.frame(width: 62, height: 62)
69-
.coverPlayButtonBackground(isEnabled: isPlayEnabled)
70104
}
71-
.disabled(!isPlayEnabled || isLoading || isLaunchingPlayback)
72-
.scaleEffect(isLaunchingPlayback ? 1.06 : 1)
73-
.animation(.easeInOut(duration: 0.18), value: isLoading)
74-
.animation(.easeInOut(duration: 0.14), value: isLaunchingPlayback)
75-
.accessibilityLabel(Text(NSLocalizedString("_play_", comment: "")))
76105
}
77106
}
78107
}
@@ -124,6 +153,8 @@ private extension View {
124153
isPlayEnabled: true,
125154
isLoading: false,
126155
isLaunchingPlayback: false,
156+
statusMessage: nil,
157+
onCancel: nil,
127158
onToggleChrome: {},
128159
onPlay: {}
129160
)
@@ -135,6 +166,8 @@ private extension View {
135166
isPlayEnabled: false,
136167
isLoading: true,
137168
isLaunchingPlayback: false,
169+
statusMessage: NSLocalizedString("_download_in_progress_", comment: ""),
170+
onCancel: {},
138171
onToggleChrome: {},
139172
onPlay: {}
140173
)

0 commit comments

Comments
 (0)