Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Subscription oauth v2 #1033

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
6 changes: 4 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ let package = Package(
"ContentBlocking",
"SecureStorage",
"Subscription",
"PixelKit"
"Networking",
"PixelKit",
],
resources: [
.process("ContentBlocking/UserScripts/contentblockerrules.js"),
Expand Down Expand Up @@ -340,7 +341,8 @@ let package = Package(
dependencies: [
.target(name: "WireGuardC"),
"Common",
"Networking"
"Networking",
"Subscription"
],
swiftSettings: [
.define("DEBUG", .when(configuration: .debug))
Expand Down
13 changes: 7 additions & 6 deletions Sources/Common/UserDefaultsCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class UserDefaultsCache<ObjectType: Codable> {
let object: ObjectType
}

let logger = { Logger(subsystem: Bundle.main.bundleIdentifier ?? "DuckDuckGo", category: "UserDefaultsCache") }()
let logger = { Logger(subsystem: "UserDefaultsCache", category: "") }()
federicocappelli marked this conversation as resolved.
Show resolved Hide resolved
private var userDefaults: UserDefaults
public private(set) var settings: UserDefaultsCacheSettings

Expand All @@ -65,8 +65,9 @@ public class UserDefaultsCache<ObjectType: Codable> {
do {
let data = try encoder.encode(cacheObject)
userDefaults.set(data, forKey: key.rawValue)
logger.debug("Cache Set: \(String(describing: cacheObject))")
logger.debug("Cache Set: \(String(describing: cacheObject), privacy: .public)")
} catch {
logger.fault("Failed to encode CacheObject: \(error, privacy: .public)")
assertionFailure("Failed to encode CacheObject: \(error)")
}
}
Expand All @@ -77,21 +78,21 @@ public class UserDefaultsCache<ObjectType: Codable> {
do {
let cacheObject = try decoder.decode(CacheObject.self, from: data)
if cacheObject.expires > Date() {
logger.debug("Cache Hit: \(ObjectType.self)")
logger.debug("Cache Hit: \(ObjectType.self, privacy: .public)")
return cacheObject.object
} else {
logger.debug("Cache Miss: \(ObjectType.self)")
logger.debug("Cache Miss: \(ObjectType.self, privacy: .public)")
reset() // Clear expired data
return nil
}
} catch let error {
logger.error("Cache Decode Error: \(error)")
logger.fault("Cache Decode Error: \(error, privacy: .public)")
return nil
}
}

public func reset() {
logger.debug("Cache Clean: \(ObjectType.self)")
logger.debug("Cache Clean: \(ObjectType.self, privacy: .public)")
userDefaults.removeObject(forKey: key.rawValue)
}
}
2 changes: 1 addition & 1 deletion Sources/DDGSync/internal/RemoteAPIRequestCreator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public struct RemoteAPIRequestCreator: RemoteAPIRequestCreating {
body: body)

if let body {
Logger.sync.debug("\(method.rawValue, privacy: .public) request body: \(String(bytes: body, encoding: .utf8) ?? "", privacy: .public)")
Logger.sync.debug("\(method.rawValue, privacy: .public) request body: \(String(bytes: body, encoding: .utf8) ?? "")")
}

return APIRequest(configuration: configuration, requirements: [.allowHTTPNotModified])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError {
case setWireguardConfig(Error)

// Auth errors
case noAuthTokenFound
case noAuthTokenFound(Error)

// Subscription errors
case vpnAccessRevoked
Expand Down Expand Up @@ -130,7 +130,6 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError {
.wireGuardCannotLocateTunnelFileDescriptor,
.wireGuardInvalidState,
.wireGuardDnsResolution,
.noAuthTokenFound,
.vpnAccessRevoked:
return [:]
case .failedToFetchServerList(let error),
Expand All @@ -149,6 +148,7 @@ public enum NetworkProtectionError: LocalizedError, CustomNSError {
.wireGuardSetNetworkSettings(let error),
.startWireGuardBackend(let error),
.setWireguardConfig(let error),
.noAuthTokenFound(let error),
.unhandledError(_, _, let error),
.failedToFetchServerStatus(let error),
.failedToParseServerStatusResponse(let error):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import Network
import Common
import Combine
import os.log
import Subscription

public actor NetworkProtectionServerStatusMonitor {

Expand Down Expand Up @@ -49,13 +50,14 @@ public actor NetworkProtectionServerStatusMonitor {
}

private let networkClient: NetworkProtectionClient
private let tokenStore: NetworkProtectionTokenStore
private let tokenProvider: any SubscriptionTokenProvider

// MARK: - Init & deinit

init(networkClient: NetworkProtectionClient, tokenStore: NetworkProtectionTokenStore) {
init(networkClient: NetworkProtectionClient,
tokenProvider: any SubscriptionTokenProvider) {
self.networkClient = networkClient
self.tokenStore = tokenStore
self.tokenProvider = tokenProvider

Logger.networkProtectionMemory.debug("[+] \(String(describing: self), privacy: .public)")
}
Expand Down Expand Up @@ -99,11 +101,11 @@ public actor NetworkProtectionServerStatusMonitor {
// MARK: - Server Status Check

private func checkServerStatus(for serverName: String) async -> Result<NetworkProtectionServerStatus, NetworkProtectionClientError> {
guard let accessToken = try? tokenStore.fetchToken() else {
guard let accessToken = try? await VPNAuthTokenBuilder.getVPNAuthToken(from: tokenProvider, policy: .localValid) else {
Logger.networkProtection.fault("Failed to check server status due to lack of access token")
assertionFailure("Failed to check server status due to lack of access token")
return .failure(.invalidAuthToken)
}

return await networkClient.getServerStatus(authToken: accessToken, serverName: serverName)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ public final class NetworkProtectionKeychainKeyStore: NetworkProtectionKeyStore
// MARK: - EventMapping

private func handle(_ error: Error) {
Logger.networkProtectionKeyManagement.error("Failed to perform operation: \(error, privacy: .public)")

guard let error = error as? NetworkProtectionKeychainStoreError else {
assertionFailure("Failed to cast Network Protection Keychain store error")
errorEvents?.fire(NetworkProtectionError.unhandledError(function: #function, line: #line, error: error))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ enum NetworkProtectionKeychainStoreError: Error, NetworkProtectionErrorConvertib
}

/// General Keychain access helper class for the NetworkProtection module. Should be used for specific KeychainStore types.
final class NetworkProtectionKeychainStore {
public final class NetworkProtectionKeychainStore {
private let label: String
private let serviceName: String
private let keychainType: KeychainType

init(label: String,
serviceName: String,
keychainType: KeychainType) {
public init(label: String,
serviceName: String,
keychainType: KeychainType) {

self.label = label
self.serviceName = serviceName
Expand All @@ -55,7 +55,8 @@ final class NetworkProtectionKeychainStore {

// MARK: - Keychain Interaction

func readData(named name: String) throws -> Data? {
public func readData(named name: String) throws -> Data? {
Logger.networkProtectionKeyManagement.debug("Reading key \(name, privacy: .public) from keychain")
var query = defaultAttributes()
query[kSecAttrAccount] = name
query[kSecReturnData] = true
Expand All @@ -78,7 +79,8 @@ final class NetworkProtectionKeychainStore {
}
}

func writeData(_ data: Data, named name: String) throws {
public func writeData(_ data: Data, named name: String) throws {
Logger.networkProtectionKeyManagement.debug("Writing key \(name, privacy: .public) to keychain")
var query = defaultAttributes()
query[kSecAttrAccount] = name
query[kSecAttrAccessible] = kSecAttrAccessibleAfterFirstUnlock
Expand All @@ -101,18 +103,20 @@ final class NetworkProtectionKeychainStore {
}

private func updateData(_ data: Data, named name: String) -> OSStatus {
Logger.networkProtectionKeyManagement.debug("Updating key \(name, privacy: .public) in keychain")
var query = defaultAttributes()
query[kSecAttrAccount] = name

let newAttributes = [
kSecValueData: data,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
kSecValueData: data,
kSecAttrAccessible: kSecAttrAccessibleAfterFirstUnlock
] as [CFString: Any]

return SecItemUpdate(query as CFDictionary, newAttributes as CFDictionary)
}

func deleteAll() throws {
public func deleteAll() throws {
Logger.networkProtectionKeyManagement.debug("Deleting all keys from keychain")
var query = defaultAttributes()
#if os(macOS)
// This line causes the delete to error with status -50 on iOS. Needs investigation but, for now, just delete the first item
Expand All @@ -125,6 +129,7 @@ final class NetworkProtectionKeychainStore {
case errSecItemNotFound, errSecSuccess:
break
default:
Logger.networkProtectionKeyManagement.error("🔴 Failed to delete all keys, SecItemDelete status \(String(describing: status), privacy: .public)")
throw NetworkProtectionKeychainStoreError.keychainDeleteError(status: status)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// NetworkProtectionKeychainTokenStore+LegacyAuthTokenStoring.swift
//
// Copyright © 2025 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import Networking

extension NetworkProtectionKeychainTokenStore: LegacyAuthTokenStoring {

public var token: String? {
get {
do {
return try fetchToken()
} catch {
assertionFailure("Failed to retrieve auth token: \(error)")
}
return nil
}
set(newValue) {
do {
guard let newValue else {
try deleteToken()
return
}
try store(newValue)
} catch {
assertionFailure("Failed set token: \(error)")
}
}
}
}
1 change: 1 addition & 0 deletions Sources/NetworkProtection/Logger+NetworkProtection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ public extension Logger {
static var networkProtectionStatusReporter = { Logger(subsystem: Logger.subsystem, category: "Status Reporter") }()
static var networkProtectionSleep = { Logger(subsystem: Logger.subsystem, category: "Sleep and Wake") }()
static var networkProtectionEntitlement = { Logger(subsystem: Logger.subsystem, category: "Entitlement Monitor") }()
static var networkProtectionWireGuard = { Logger(subsystem: Logger.subsystem, category: "WireGuardAdapter") }()
}
32 changes: 20 additions & 12 deletions Sources/NetworkProtection/NetworkProtectionDeviceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Foundation
import Common
import NetworkExtension
import os.log
import Subscription

public enum NetworkProtectionServerSelectionMethod: CustomDebugStringConvertible {
public var debugDescription: String {
Expand Down Expand Up @@ -73,27 +74,27 @@ public protocol NetworkProtectionDeviceManagement {

public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement {
private let networkClient: NetworkProtectionClient
private let tokenStore: NetworkProtectionTokenStore
private let tokenProvider: any SubscriptionTokenProvider
private let keyStore: NetworkProtectionKeyStore

private let errorEvents: EventMapping<NetworkProtectionError>?

public init(environment: VPNSettings.SelectedEnvironment,
tokenStore: NetworkProtectionTokenStore,
tokenProvider: any SubscriptionTokenProvider,
keyStore: NetworkProtectionKeyStore,
errorEvents: EventMapping<NetworkProtectionError>?) {
self.init(networkClient: NetworkProtectionBackendClient(environment: environment),
tokenStore: tokenStore,
tokenProvider: tokenProvider,
keyStore: keyStore,
errorEvents: errorEvents)
}

init(networkClient: NetworkProtectionClient,
tokenStore: NetworkProtectionTokenStore,
tokenProvider: any SubscriptionTokenProvider,
keyStore: NetworkProtectionKeyStore,
errorEvents: EventMapping<NetworkProtectionError>?) {
self.networkClient = networkClient
self.tokenStore = tokenStore
self.tokenProvider = tokenProvider
self.keyStore = keyStore
self.errorEvents = errorEvents
}
Expand All @@ -102,8 +103,11 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement {
/// This method will return the remote server list if available, or the local server list if there was a problem with the service call.
///
public func refreshServerList() async throws -> [NetworkProtectionServer] {
guard let token = try? tokenStore.fetchToken() else {
throw NetworkProtectionError.noAuthTokenFound
let token: String
do {
token = try await VPNAuthTokenBuilder.getVPNAuthToken(from: tokenProvider, policy: .localValid)
} catch {
throw NetworkProtectionError.noAuthTokenFound(error)
}
let result = await networkClient.getServers(authToken: token)
let completeServerList: [NetworkProtectionServer]
Expand Down Expand Up @@ -188,8 +192,12 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement {
private func register(keyPair: KeyPair,
selectionMethod: NetworkProtectionServerSelectionMethod) async throws -> (server: NetworkProtectionServer,
newExpiration: Date?) {

guard let token = try? tokenStore.fetchToken() else { throw NetworkProtectionError.noAuthTokenFound }
let token: String
do {
token = try await VPNAuthTokenBuilder.getVPNAuthToken(from: tokenProvider, policy: .localValid)
} catch {
throw NetworkProtectionError.noAuthTokenFound(error)
}

let serverSelection: RegisterServerSelection
let excludedServerName: String?
Expand Down Expand Up @@ -313,11 +321,11 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement {
}

private func handle(clientError: NetworkProtectionClientError) {
#if os(macOS)
#if os(macOS)
if case .invalidAuthToken = clientError {
try? tokenStore.deleteToken()
tokenProvider.removeTokenContainer()
}
#endif
#endif
errorEvents?.fire(clientError.networkProtectionError)
}

Expand Down
1 change: 1 addition & 0 deletions Sources/NetworkProtection/NetworkProtectionOptionKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum NetworkProtectionOptionKey {
public static let dnsSettings = "dnsSettings"
public static let excludeLocalNetworks = "excludeLocalNetworks"
public static let authToken = "authToken"
public static let tokenContainer = "tokenContainer"
public static let isOnDemand = "is-on-demand"
public static let activationAttemptId = "activationAttemptId"
public static let tunnelFailureSimulation = "tunnelFailureSimulation"
Expand Down
20 changes: 20 additions & 0 deletions Sources/NetworkProtection/Networking/NetworkProtectionClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,26 @@ public enum NetworkProtectionClientError: CustomNSError, NetworkProtectionErrorC
return [:]
}
}

public var errorDescription: String? {
switch self {
case .failedToFetchLocationList: return "Failed to fetch location list"
case .failedToParseLocationListResponse: return "Failed to parse location list response"
case .failedToFetchServerList: return "Failed to fetch server list"
case .failedToParseServerListResponse: return "Failed to parse server list response"
case .failedToEncodeRegisterKeyRequest: return "Failed to encode register key request"
case .failedToFetchServerStatus(let error):
return "Failed to fetch server status: \(error)"
case .failedToParseServerStatusResponse(let error):
return "Failed to parse server status response: \(error)"
case .failedToFetchRegisteredServers(let error):
return "Failed to fetch registered servers: \(error)"
case .failedToParseRegisteredServersResponse(let error):
return "Failed to parse registered servers response: \(error)"
case .invalidAuthToken: return "Invalid auth token"
case .accessDenied: return "Access denied"
}
}
}

struct RegisterKeyRequestBody: Encodable {
Expand Down
Loading
Loading