Skip to content

Commit

Permalink
Merge branch 'main' into brindy/add-history-to-ios
Browse files Browse the repository at this point in the history
  • Loading branch information
brindy committed Mar 5, 2024
2 parents c1a8ea4 + fd8dc25 commit d0b2e23
Show file tree
Hide file tree
Showing 26 changed files with 526 additions and 36 deletions.
2 changes: 1 addition & 1 deletion .xcode-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
15.1
15.2
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/content-scope-scripts",
"state" : {
"revision" : "a3690b7666a3617693383d948cb492513f6aa569",
"version" : "5.0.0"
"revision" : "59752eb7973d3e3b0c23255ff51359f48b343f15",
"version" : "5.2.0"
}
},
{
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ let package = Package(
.package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.2.0"),
.package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "2.1.0"),
.package(url: "https://github.com/duckduckgo/privacy-dashboard", exact: "3.2.0"),
.package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "5.0.0"),
.package(url: "https://github.com/duckduckgo/content-scope-scripts", exact: "5.2.0"),
.package(url: "https://github.com/httpswift/swifter.git", exact: "1.5.0"),
.package(url: "https://github.com/duckduckgo/bloom_cpp.git", exact: "3.0.0"),
.package(url: "https://github.com/duckduckgo/wireguard-apple", exact: "1.1.1"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public protocol AutofillDatabaseProvider: SecureStorageDatabaseProvider {
func websiteAccountsForDomain(_ domain: String) throws -> [SecureVaultModels.WebsiteAccount]
func websiteAccountsForTopLevelDomain(_ eTLDplus1: String) throws -> [SecureVaultModels.WebsiteAccount]
func deleteWebsiteCredentialsForAccountId(_ accountId: Int64) throws
func deleteAllWebsiteCredentials() throws

func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites]
func hasNeverPromptWebsitesFor(domain: String) throws -> Bool
Expand Down Expand Up @@ -216,6 +217,16 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro
""", arguments: [accountId])
}

public func deleteAllWebsiteCredentials() throws {
try db.write {
try updateSyncTimestampForAllObjects(in: $0, tableName: SecureVaultModels.SyncableCredentialsRecord.databaseTableName)
try $0.execute(sql: """
DELETE FROM
\(SecureVaultModels.WebsiteAccount.databaseTableName)
""")
}
}

func updateWebsiteCredentials(in database: Database,
_ credentials: SecureVaultModels.WebsiteCredentials,
usingId id: Int64,
Expand Down Expand Up @@ -569,6 +580,18 @@ public final class DefaultAutofillDatabaseProvider: GRDBSecureStorageDatabasePro
""", arguments: [timestamp?.withMillisecondPrecision, objectId])
}

public func updateSyncTimestampForAllObjects(in database: Database, tableName: String, timestamp: Date? = Date()) throws {
assert(database.isInsideTransaction)

try database.execute(sql: """
UPDATE
\(tableName)
SET
\(SecureVaultSyncableColumns.lastModified.name) = ?
""", arguments: [timestamp?.withMillisecondPrecision])
}

}

extension Date {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public protocol AutofillSecureVault: SecureVault {
@discardableResult
func storeWebsiteCredentials(_ credentials: SecureVaultModels.WebsiteCredentials) throws -> Int64
func deleteWebsiteCredentialsFor(accountId: Int64) throws
func deleteAllWebsiteCredentials() throws

func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites]
func hasNeverPromptWebsitesFor(domain: String) throws -> Bool
Expand Down Expand Up @@ -365,6 +366,12 @@ public class DefaultAutofillSecureVault<T: AutofillDatabaseProvider>: AutofillSe
}
}

public func deleteAllWebsiteCredentials() throws {
try executeThrowingDatabaseOperation {
try self.providers.database.deleteAllWebsiteCredentials()
}
}

// MARK: NeverPromptWebsites

public func neverPromptWebsites() throws -> [SecureVaultModels.NeverPromptWebsites] {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Common/Debug.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public func callingSymbol() -> String {
repeat {
// caller for the procedure
callingSymbolIdx += 1
let line = stackTrace[callingSymbolIdx].replacingOccurrences(of: Bundle.main.name!, with: "DDG")
let line = stackTrace[callingSymbolIdx].replacingOccurrences(of: Bundle.main.executableURL!.lastPathComponent, with: "DDG")
symbolName = String(line.split(separator: " ", maxSplits: 3)[3]).components(separatedBy: " + ")[0]
} while stackTrace[callingSymbolIdx - 1].contains(symbolName.dropping(suffix: "To")) // skip objc wrappers

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// NetworkProtectionEntitlementMonitor.swift
//
// Copyright © 2024 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 Common

public actor NetworkProtectionEntitlementMonitor {
public enum Result {
case validEntitlement
case invalidEntitlement
case error(Error)
}

private static let monitoringInterval: TimeInterval = .minutes(20)

private var task: Task<Never, Error>? {
willSet {
task?.cancel()
}
}

var isStarted: Bool {
task?.isCancelled == false
}

// MARK: - Init & deinit

init() {
os_log("[+] %{public}@", log: .networkProtectionMemoryLog, type: .debug, String(describing: self))
}

deinit {
task?.cancel()

os_log("[-] %{public}@", log: .networkProtectionMemoryLog, type: .debug, String(describing: self))
}

// MARK: - Start/Stop monitoring

public func start(entitlementCheck: @escaping () async -> Swift.Result<Bool, Error>, callback: @escaping (Result) -> Void) {
os_log("⚫️ Starting entitlement monitor", log: .networkProtectionEntitlementMonitorLog)

task = Task.periodic(interval: Self.monitoringInterval) {
let result = await entitlementCheck()
switch result {
case .success(let hasEntitlement):
if hasEntitlement {
os_log("⚫️ Valid entitlement", log: .networkProtectionEntitlementMonitorLog)
callback(.validEntitlement)
} else {
os_log("⚫️ Invalid entitlement", log: .networkProtectionEntitlementMonitorLog)
callback(.invalidEntitlement)
}
case .failure(let error):
os_log("⚫️ Error retrieving entitlement: %{public}@", log: .networkProtectionEntitlementMonitorLog, error.localizedDescription)
callback(.error(error))
}
}
}

public func stop() {
os_log("⚫️ Stopping entitlement monitor", log: .networkProtectionEntitlementMonitorLog)

task = nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public enum NetworkProtectionError: LocalizedError {
// Auth errors
case noAuthTokenFound

// Subscription errors
case vpnAccessRevoked

// Unhandled error
case unhandledError(function: String, line: Int, error: Error)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ public final class NetworkProtectionCodeRedemptionCoordinator: NetworkProtection
tokenStore: NetworkProtectionTokenStore,
versionStore: NetworkProtectionLastVersionRunStore = .init(),
isManualCodeRedemptionFlow: Bool = false,
errorEvents: EventMapping<NetworkProtectionError>) {
self.init(networkClient: NetworkProtectionBackendClient(environment: environment),
errorEvents: EventMapping<NetworkProtectionError>,
isSubscriptionEnabled: Bool) {
self.init(networkClient: NetworkProtectionBackendClient(environment: environment, isSubscriptionEnabled: isSubscriptionEnabled),
tokenStore: tokenStore,
versionStore: versionStore,
isManualCodeRedemptionFlow: isManualCodeRedemptionFlow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,27 @@ public protocol NetworkProtectionTokenStore {
///
func fetchToken() throws -> String?

/// Obtain the stored auth token.
/// Delete the stored auth token.
///
func deleteToken() throws

/// Convert DDG-access-token to NetP-auth-token
///
/// todo - https://app.asana.com/0/0/1206541966681608/f
static func makeToken(from subscriptionAccessToken: String) -> String

/// Check if a given token is derived from DDG-access-token
///
/// todo - https://app.asana.com/0/0/1206541966681608/f
static func isSubscriptionAccessToken(_ token: String) -> Bool
}

/// Store an auth token for NetworkProtection on behalf of the user. This key is then used to authenticate requests for registration and server fetches from the Network Protection backend servers.
/// Writing a new auth token will replace the old one.
public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenStore {
private let keychainStore: NetworkProtectionKeychainStore
private let errorEvents: EventMapping<NetworkProtectionError>?
private let isSubscriptionEnabled: Bool

public struct Defaults {
static let tokenStoreEntryLabel = "DuckDuckGo Network Protection Auth Token"
Expand All @@ -48,11 +59,13 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt

public init(keychainType: KeychainType,
serviceName: String = Defaults.tokenStoreService,
errorEvents: EventMapping<NetworkProtectionError>?) {
errorEvents: EventMapping<NetworkProtectionError>?,
isSubscriptionEnabled: Bool) {
keychainStore = NetworkProtectionKeychainStore(label: Defaults.tokenStoreEntryLabel,
serviceName: serviceName,
keychainType: keychainType)
self.errorEvents = errorEvents
self.isSubscriptionEnabled = isSubscriptionEnabled
}

public func store(_ token: String) throws {
Expand All @@ -78,6 +91,8 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt

public func deleteToken() throws {
do {
// Skip deleting DDG-access-token as it's used for entitlement validity check
guard isSubscriptionEnabled, let token = try? fetchToken(), !Self.isSubscriptionAccessToken(token) else { return }
try keychainStore.deleteAll()
} catch {
handle(error)
Expand All @@ -97,3 +112,15 @@ public final class NetworkProtectionKeychainTokenStore: NetworkProtectionTokenSt
errorEvents?.fire(error.networkProtectionError)
}
}

extension NetworkProtectionTokenStore {
private static var authTokenPrefix: String { "ddg:" }

public static func makeToken(from subscriptionAccessToken: String) -> String {
"\(authTokenPrefix)\(subscriptionAccessToken)"
}

public static func isSubscriptionAccessToken(_ token: String) -> Bool {
token.hasPrefix(authTokenPrefix)
}
}
6 changes: 6 additions & 0 deletions Sources/NetworkProtection/Logging/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ extension OSLog {
public static var networkProtectionSleepLog: OSLog {
Logging.networkProtectionSleepLoggingEnabled ? Logging.networkProtectionSleepLog : .disabled
}

public static var networkProtectionEntitlementMonitorLog: OSLog {
Logging.networkProtectionEntitlementLoggingEnabled ? Logging.networkProtectionEntitlementLog : .disabled
}
}

// swiftlint:disable line_length
Expand Down Expand Up @@ -111,5 +115,7 @@ struct Logging {
fileprivate static let networkProtectionSleepLoggingEnabled = true
fileprivate static let networkProtectionSleepLog: OSLog = OSLog(subsystem: subsystem, category: "Network Protection: Sleep and Wake")

fileprivate static let networkProtectionEntitlementLoggingEnabled = true
fileprivate static let networkProtectionEntitlementLog: OSLog = OSLog(subsystem: subsystem, category: "Network Protection: Entitlement Monitor")
}
// swiftlint:enable line_length
21 changes: 17 additions & 4 deletions Sources/NetworkProtection/NetworkProtectionDeviceManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,28 +58,34 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement {

private let errorEvents: EventMapping<NetworkProtectionError>?

private let isSubscriptionEnabled: Bool

public init(environment: VPNSettings.SelectedEnvironment,
tokenStore: NetworkProtectionTokenStore,
keyStore: NetworkProtectionKeyStore,
serverListStore: NetworkProtectionServerListStore? = nil,
errorEvents: EventMapping<NetworkProtectionError>?) {
self.init(networkClient: NetworkProtectionBackendClient(environment: environment),
errorEvents: EventMapping<NetworkProtectionError>?,
isSubscriptionEnabled: Bool) {
self.init(networkClient: NetworkProtectionBackendClient(environment: environment, isSubscriptionEnabled: isSubscriptionEnabled),
tokenStore: tokenStore,
keyStore: keyStore,
serverListStore: serverListStore,
errorEvents: errorEvents)
errorEvents: errorEvents,
isSubscriptionEnabled: isSubscriptionEnabled)
}

init(networkClient: NetworkProtectionClient,
tokenStore: NetworkProtectionTokenStore,
keyStore: NetworkProtectionKeyStore,
serverListStore: NetworkProtectionServerListStore? = nil,
errorEvents: EventMapping<NetworkProtectionError>?) {
errorEvents: EventMapping<NetworkProtectionError>?,
isSubscriptionEnabled: Bool) {
self.networkClient = networkClient
self.tokenStore = tokenStore
self.keyStore = keyStore
self.serverListStore = serverListStore ?? NetworkProtectionServerListFileSystemStore(errorEvents: errorEvents)
self.errorEvents = errorEvents
self.isSubscriptionEnabled = isSubscriptionEnabled
}

/// Requests a new server list from the backend and updates it locally.
Expand Down Expand Up @@ -169,6 +175,8 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement {
// - keyPair: the key pair that was used to register with the server, and that should be used to configure the tunnel
//
// - Throws:`NetworkProtectionError`
//
// swiftlint:disable:next cyclomatic_complexity
private func register(keyPair: KeyPair,
selectionMethod: NetworkProtectionServerSelectionMethod) async throws -> (server: NetworkProtectionServer,
newExpiration: Date?) {
Expand Down Expand Up @@ -224,6 +232,11 @@ public actor NetworkProtectionDeviceManager: NetworkProtectionDeviceManagement {
selectedServer = registeredServer
return (selectedServer, selectedServer.expirationDate)
case .failure(let error):
if isSubscriptionEnabled, case .accessDenied = error {
errorEvents?.fire(.vpnAccessRevoked)
throw NetworkProtectionError.vpnAccessRevoked
}

handle(clientError: error)

let cachedServer = try cachedServer(registeredWith: keyPair)
Expand Down
16 changes: 13 additions & 3 deletions Sources/NetworkProtection/Networking/NetworkProtectionClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public enum NetworkProtectionClientError: Error, NetworkProtectionErrorConvertib
case failedToRetrieveAuthToken(AuthenticationFailureResponse)
case failedToParseRedeemResponse(Error)
case invalidAuthToken
case accessDenied

var networkProtectionError: NetworkProtectionError {
switch self {
Expand All @@ -61,6 +62,7 @@ public enum NetworkProtectionClientError: Error, NetworkProtectionErrorConvertib
case .failedToRetrieveAuthToken(let response): return .failedToRetrieveAuthToken(response)
case .failedToParseRedeemResponse(let error): return .failedToParseRedeemResponse(error)
case .invalidAuthToken: return .invalidAuthToken
case .accessDenied: return .vpnAccessRevoked
}
}
}
Expand Down Expand Up @@ -164,9 +166,17 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
}()

private let endpointURL: URL
private let isSubscriptionEnabled: Bool

init(environment: VPNSettings.SelectedEnvironment = .default) {
endpointURL = environment.endpointURL
init(environment: VPNSettings.SelectedEnvironment = .default, isSubscriptionEnabled: Bool) {
self.isSubscriptionEnabled = isSubscriptionEnabled

// todo - https://app.asana.com/0/0/1206470585910129/f
if isSubscriptionEnabled {
self.endpointURL = URL(string: "https://staging1.netp.duckduckgo.com")!
} else {
self.endpointURL = environment.endpointURL
}
}

func getLocations(authToken: String) async -> Result<[NetworkProtectionLocation], NetworkProtectionClientError> {
Expand Down Expand Up @@ -249,7 +259,7 @@ final class NetworkProtectionBackendClient: NetworkProtectionClient {
switch response.statusCode {
case 200: responseData = data
case 401: return .failure(.invalidAuthToken)
default: return .failure(.failedToFetchRegisteredServers(nil))
default: return (response.statusCode == 403 && isSubscriptionEnabled) ? .failure(.accessDenied) : .failure(.failedToFetchRegisteredServers(nil))
}
} catch {
return .failure(NetworkProtectionClientError.failedToFetchRegisteredServers(error))
Expand Down
Loading

0 comments on commit d0b2e23

Please sign in to comment.