1
-import Foundation
2
-import Security
3
-
4
-public enum SmbCloudCredentialsManagerError: Error, LocalizedError, Sendable {
5
- case encodingFailed(String)
6
- case decodingFailed(String)
7
- case keychainOperationFailed(status: OSStatus, message: String)
8
-
9
- public var errorDescription: String? {
10
- switch self {
11
- case .encodingFailed(let message):
12
- return message
13
- case .decodingFailed(let message):
14
- return message
15
- case .keychainOperationFailed(_, let message):
16
- return message
17
- }
18
- }
19
-}
20
-
21
-public struct SmbCloudCredentialsManager {
22
- public let service: String
23
- public let account: String
24
- public let accessGroup: String?
25
-
26
- public init(
27
- service: String? = nil,
28
- account: String = "current",
29
- accessGroup: String? = nil
30
- ) {
31
- self.service = service?.nilIfEmpty ?? Self.defaultServiceName
32
- self.account = account
33
- self.accessGroup = accessGroup?.nilIfEmpty
34
- }
35
-
36
- public func store(_ session: SmbCloudSession) throws {
37
- let encodedSession: Data
38
- do {
39
- encodedSession = try JSONEncoder().encode(session.storedRepresentation())
40
- } catch {
41
- throw SmbCloudCredentialsManagerError.encodingFailed(
42
- "Failed to encode smbCloud session: \(error.localizedDescription)"
43
- )
44
- }
45
-
46
- var addQuery = baseQuery()
47
- addQuery[kSecValueData as String] = encodedSession
48
- addSecureAccessibility(to: &addQuery)
49
-
50
- let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
51
- switch addStatus {
52
- case errSecSuccess:
53
- return
54
- case errSecDuplicateItem:
55
- let attributesToUpdate = [
56
- kSecValueData as String: encodedSession
57
- ]
58
- let updateStatus = SecItemUpdate(
59
- baseQuery() as CFDictionary, attributesToUpdate as CFDictionary)
60
- guard updateStatus == errSecSuccess else {
61
- throw keychainError(for: updateStatus)
1
+#if canImport(Security)
2
+ import AuthCore
3
+ import Foundation
4
+ import Security
5
+
6
+ public enum SmbCloudCredentialsManagerError: Error, LocalizedError, Sendable {
7
+ case encodingFailed(String)
8
+ case decodingFailed(String)
9
+ case keychainOperationFailed(status: OSStatus, message: String)
10
+
11
+ public var errorDescription: String? {
12
+ switch self {
13
+ case .encodingFailed(let message):
14
+ return message
15
+ case .decodingFailed(let message):
16
+ return message
17
+ case .keychainOperationFailed(_, let message):
18
+ return message
19
}
63
- default:
64
- throw keychainError(for: addStatus)
20
}
21
}
22
68
- public func current() throws -> SmbCloudSession? {
69
- var query = baseQuery()
70
- query[kSecReturnData as String] = true
71
- query[kSecMatchLimit as String] = kSecMatchLimitOne
72
-
73
- var item: CFTypeRef?
74
- let status = SecItemCopyMatching(query as CFDictionary, &item)
75
- switch status {
76
- case errSecSuccess:
77
- guard let data = item as? Data else {
78
- throw SmbCloudCredentialsManagerError.decodingFailed(
79
- "The keychain item did not contain valid session data."
80
- )
81
- }
23
+ /// Keychain-backed ``SmbCloudCredentialsStore`` for Apple platforms.
24
+ ///
25
+ /// This type is only available where the `Security` framework exists
26
+ /// (iOS, macOS, tvOS, watchOS, visionOS). On other platforms, conform your
27
+ /// own type to ``SmbCloudCredentialsStore`` or use
28
+ /// ``SmbCloudInMemoryCredentialsStore`` from `AuthCore`.
29
+ public struct SmbCloudCredentialsManager: SmbCloudCredentialsStore {
30
+ public let service: String
31
+ public let account: String
32
+ public let accessGroup: String?
33
+
34
+ public init(
35
+ service: String? = nil,
36
+ account: String = "current",
37
+ accessGroup: String? = nil
38
+ ) {
39
+ self.service = service?.smbCloudNilIfBlank ?? Self.defaultServiceName
40
+ self.account = account
41
+ self.accessGroup = accessGroup?.smbCloudNilIfBlank
42
+ }
43
44
+ public func store(_ session: SmbCloudSession) throws {
45
+ let encodedSession: Data
46
do {
84
- return try JSONDecoder().decode(SmbCloudSession.self, from: data)
85
- .storedRepresentation()
47
+ encodedSession = try JSONEncoder().encode(session.storedRepresentation())
48
} catch {
87
- throw SmbCloudCredentialsManagerError.decodingFailed(
88
- "Failed to decode smbCloud session: \(error.localizedDescription)"
49
+ throw SmbCloudCredentialsManagerError.encodingFailed(
50
+ "Failed to encode smbCloud session: \(error.localizedDescription)"
51
)
52
}
91
- case errSecItemNotFound:
92
- return nil
93
- default:
94
- throw keychainError(for: status)
95
- }
96
- }
53
98
- public func currentValidSession(leeway: TimeInterval = 60) throws -> SmbCloudSession? {
99
- guard let session = try current() else {
100
- return nil
54
+ var addQuery = baseQuery()
55
+ addQuery[kSecValueData as String] = encodedSession
56
+ addSecureAccessibility(to: &addQuery)
57
+
58
+ let addStatus = SecItemAdd(addQuery as CFDictionary, nil)
59
+ switch addStatus {
60
+ case errSecSuccess:
61
+ return
62
+ case errSecDuplicateItem:
63
+ let attributesToUpdate = [
64
+ kSecValueData as String: encodedSession
65
+ ]
66
+ let updateStatus = SecItemUpdate(
67
+ baseQuery() as CFDictionary, attributesToUpdate as CFDictionary)
68
+ guard updateStatus == errSecSuccess else {
69
+ throw keychainError(for: updateStatus)
70
+ }
71
+ default:
72
+ throw keychainError(for: addStatus)
73
+ }
74
}
75
103
- return session.isValid(leeway: leeway) ? session : nil
104
- }
105
-
106
- public func hasValidSession(leeway: TimeInterval = 60) throws -> Bool {
107
- try currentValidSession(leeway: leeway) != nil
108
- }
76
+ public func current() throws -> SmbCloudSession? {
77
+ var query = baseQuery()
78
+ query[kSecReturnData as String] = true
79
+ query[kSecMatchLimit as String] = kSecMatchLimitOne
80
+
81
+ var item: CFTypeRef?
82
+ let status = SecItemCopyMatching(query as CFDictionary, &item)
83
+ switch status {
84
+ case errSecSuccess:
85
+ guard let data = item as? Data else {
86
+ throw SmbCloudCredentialsManagerError.decodingFailed(
87
+ "The keychain item did not contain valid session data."
88
+ )
89
+ }
90
+
91
+ do {
92
+ return try JSONDecoder().decode(SmbCloudSession.self, from: data)
93
+ .storedRepresentation()
94
+ } catch {
95
+ throw SmbCloudCredentialsManagerError.decodingFailed(
96
+ "Failed to decode smbCloud session: \(error.localizedDescription)"
97
+ )
98
+ }
99
+ case errSecItemNotFound:
100
+ return nil
101
+ default:
102
+ throw keychainError(for: status)
103
+ }
104
+ }
105
110
- public func clear() throws {
111
- let status = SecItemDelete(baseQuery() as CFDictionary)
112
- switch status {
113
- case errSecSuccess, errSecItemNotFound:
114
- return
115
- default:
116
- throw keychainError(for: status)
106
+ public func clear() throws {
107
+ let status = SecItemDelete(baseQuery() as CFDictionary)
108
+ switch status {
109
+ case errSecSuccess, errSecItemNotFound:
110
+ return
111
+ default:
112
+ throw keychainError(for: status)
113
+ }
114
}
115
}
119
-}
116
121
-extension SmbCloudCredentialsManager {
122
- fileprivate static var defaultServiceName: String {
123
- if let bundleIdentifier = Bundle.main.bundleIdentifier?.nilIfEmpty {
124
- return "\(bundleIdentifier).smbcloud.auth"
117
+ extension SmbCloudCredentialsManager {
118
+ fileprivate static var defaultServiceName: String {
119
+ if let bundleIdentifier = Bundle.main.bundleIdentifier?.smbCloudNilIfBlank {
120
+ return "\(bundleIdentifier).smbcloud.auth"
121
+ }
122
+
123
+ return "xyz.smbcloud.auth"
124
}
125
127
- return "xyz.smbcloud.auth"
128
- }
126
+ fileprivate func baseQuery() -> [String: Any] {
127
+ var query: [String: Any] = [
128
+ kSecClass as String: kSecClassGenericPassword,
129
+ kSecAttrService as String: service,
130
+ kSecAttrAccount as String: account,
131
+ ]
132
130
- fileprivate func baseQuery() -> [String: Any] {
131
- var query: [String: Any] = [
132
- kSecClass as String: kSecClassGenericPassword,
133
- kSecAttrService as String: service,
134
- kSecAttrAccount as String: account,
135
- ]
133
+ if let accessGroup {
134
+ query[kSecAttrAccessGroup as String] = accessGroup
135
+ }
136
137
- if let accessGroup {
138
- query[kSecAttrAccessGroup as String] = accessGroup
137
+ return query
138
}
139
141
- return query
142
- }
140
+ fileprivate func addSecureAccessibility(to query: inout [String: Any]) {
141
+ #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
142
+ query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
143
+ #endif
144
+ }
145
144
- fileprivate func addSecureAccessibility(to query: inout [String: Any]) {
145
- #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS)
146
- query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
147
- #endif
146
+ fileprivate func keychainError(for status: OSStatus) -> SmbCloudCredentialsManagerError {
147
+ let message =
148
+ SecCopyErrorMessageString(status, nil) as String?
149
+ ?? "Keychain operation failed with status \(status)."
150
+ return .keychainOperationFailed(status: status, message: message)
151
+ }
152
}
153
150
- fileprivate func keychainError(for status: OSStatus) -> SmbCloudCredentialsManagerError {
151
- let message =
152
- SecCopyErrorMessageString(status, nil) as String?
153
- ?? "Keychain operation failed with status \(status)."
154
- return .keychainOperationFailed(status: status, message: message)
154
+ extension String {
155
+ /// Module-local helper mirroring AuthCore's internal `nilIfEmpty`.
156
+ fileprivate var smbCloudNilIfBlank: String? {
157
+ trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? nil : self
158
+ }
159
}
156
-}
160
+#endif