Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into anya/sync-creds-adapt…
Browse files Browse the repository at this point in the history
…er-vault-init

# Conflicts:
#	DuckDuckGo.xcodeproj/project.pbxproj
#	DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • Loading branch information
amddg44 committed Jan 8, 2025
2 parents f8d2dc8 + 5591a5a commit 529c099
Show file tree
Hide file tree
Showing 21 changed files with 587 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ final class SecureVaultReporter: SecureVaultReporting {
guard !ProcessInfo().arguments.contains("testing") else { return }
#endif
let pixelParams = [PixelParameters.isBackgrounded: "false",
PixelParameters.appVersion: AppVersion.shared.versionAndBuildNumber]
PixelParameters.appVersion: AppVersion.shared.versionAndBuildNumber,
PixelParameters.isExtension: "true"]

switch error {
case .initFailed(let error):
Expand Down
2 changes: 1 addition & 1 deletion Configuration/Version.xcconfig
Original file line number Diff line number Diff line change
@@ -1 +1 @@
MARKETING_VERSION = 7.151.0
MARKETING_VERSION = 7.152.0
4 changes: 2 additions & 2 deletions Core/AppPrivacyConfigurationDataProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import BrowserServicesKit
final public class AppPrivacyConfigurationDataProvider: EmbeddedDataProvider {

public struct Constants {
public static let embeddedDataETag = "\"c065d77250bce32110b6f66be0ddf36d\""
public static let embeddedDataSHA = "eb30059bd7dd769eb9b5546ea345305254cc4bd93ee285b9199e0e556174c2e8"
public static let embeddedDataETag = "\"dc4fc236d233883d9391c439e05ab610\""
public static let embeddedDataSHA = "8af2ec9ba8b7d6393c87b389d8e3ea6f770ffa3a9dee97f9e06e5e4c8fcaae23"
}

public var embeddedDataEtag: String {
Expand Down
199 changes: 199 additions & 0 deletions Core/AutofillVaultKeychainMigrator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
//
// AutofillVaultKeychainMigrator.swift
// DuckDuckGo
//
// 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 BrowserServicesKit
import os.log
import Persistence
import GRDB
import SecureStorage

public protocol AutofillKeychainService {
func hasKeychainItemsMatching(serviceName: String) -> Bool
func deleteKeychainItems(matching serviceName: String)
}

public struct DefaultAutofillKeychainService: AutofillKeychainService {

public init() {}

public func hasKeychainItemsMatching(serviceName: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecReturnAttributes as String: true,
kSecMatchLimit as String: kSecMatchLimitAll
]

var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)

if status == errSecSuccess, let items = result as? [[String: Any]] {
for item in items {
if let service = item[kSecAttrService as String] as? String,
service.lowercased() == serviceName.lowercased() {
Logger.autofill.debug("Found keychain items matching service name: \(serviceName)")
return true
}
}
}

return false
}

public func deleteKeychainItems(matching serviceName: String) {
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: serviceName
]

let deleteStatus = SecItemDelete(deleteQuery as CFDictionary)
if deleteStatus == errSecSuccess {
Logger.autofill.debug("Deleted keychain item: \(serviceName)")
} else {
Logger.autofill.debug("Failed to delete keychain item: \(serviceName), error: \(deleteStatus)")
}
}
}

public class AutofillVaultKeychainMigrator {

internal let keychainService: AutofillKeychainService

private let store: KeyValueStoring

@UserDefaultsWrapper(key: .autofillVaultMigrated, defaultValue: false)
var wasVaultMigrated: Bool

private var vaultMigrated: Bool {
get {
return (store.object(forKey: UserDefaultsWrapper<Bool>.Key.autofillVaultMigrated.rawValue) as? Bool) ?? false
}
set {
store.set(newValue, forKey: UserDefaultsWrapper<Bool>.Key.autofillVaultMigrated.rawValue)
}
}

public init(keychainService: AutofillKeychainService = DefaultAutofillKeychainService(), store: KeyValueStoring = UserDefaults.app) {
self.keychainService = keychainService
self.store = store
}

public func resetVaultMigrationIfRequired(fileManager: FileManager = FileManager.default) {
guard !vaultMigrated else {
return
}

let originalVaultLocation = DefaultAutofillDatabaseProvider.defaultDatabaseURL()
let sharedVaultLocation = DefaultAutofillDatabaseProvider.defaultSharedDatabaseURL()
let hasV4Items = keychainService.hasKeychainItemsMatching(serviceName: "DuckDuckGo Secure Vault v4")

// only care about users who have have both the original and migrated vaults, as well as v4 keychain items
guard fileManager.fileExists(atPath: originalVaultLocation.path),
fileManager.fileExists(atPath: sharedVaultLocation.path),
hasV4Items else {
vaultMigrated = true
return
}

let hasV3Items = keychainService.hasKeychainItemsMatching(serviceName: "DuckDuckGo Secure Vault v3")
let hasV2Items = keychainService.hasKeychainItemsMatching(serviceName: "DuckDuckGo Secure Vault v2")
let hasV1Items = keychainService.hasKeychainItemsMatching(serviceName: "DuckDuckGo Secure Vault")

// Only continue if there are original keychain items to migrate from
guard hasV1Items || hasV2Items || hasV3Items else {
vaultMigrated = true
return
}

let backupFilePath = sharedVaultLocation.appendingPathExtension("bak")
do {
// only complete the migration if the shared database is empty
let databaseIsEmpty = try databaseIsEmpty(at: sharedVaultLocation)
if !databaseIsEmpty {
Pixel.fire(pixel: .secureVaultV4MigrationSkipped)
} else {
// Creating a backup of the migrated file
try fileManager.moveItem(at: sharedVaultLocation, to: backupFilePath)
keychainService.deleteKeychainItems(matching: "DuckDuckGo Secure Vault v4")
Pixel.fire(pixel: .secureVaultV4Migration)
}
wasVaultMigrated = true
} catch {
Logger.autofill.error("Failed to create backup of migrated file: \(error.localizedDescription)")
return
}
}

internal func databaseIsEmpty(at url: URL) throws -> Bool {
let keyStoreProvider: SecureStorageKeyStoreProvider = AutofillSecureVaultFactory.makeKeyStoreProvider(nil)
guard let existingL1Key = try keyStoreProvider.l1Key() else {
return false
}

var config = Configuration()
config.prepareDatabase {
try $0.usePassphrase(existingL1Key)
}

let dbQueue = try DatabaseQueue(path: url.path, configuration: config)

try dbQueue.write { db in
try db.usePassphrase(existingL1Key)
}

let isEmpty = try dbQueue.read { db in
// Find all user-created tables (excluding system tables)
let tableNames = try Row.fetchAll(
db,
sql: """
SELECT name
FROM sqlite_master
WHERE type='table'
AND name NOT LIKE 'sqlite_%'
"""
).map { row -> String in
row["name"]
}

// No user tables at all -> definitely empty
if tableNames.isEmpty {
return true
}

// Check each table for rows
for table in tableNames {
if table == "grdb_migrations" {
continue
}

let rowCount = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM \(table)") ?? 0
if rowCount > 0 {
// Found data in at least one table -> not empty
return false
}
}

// There's at least one user table, but all are empty
return true
}

return isEmpty
}

}
1 change: 1 addition & 0 deletions Core/Pixel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public struct PixelParameters {
// Autofill
public static let countBucket = "count_bucket"
public static let backfilled = "backfilled"
public static let isExtension = "is_extension"

// Privacy Dashboard
public static let daysSinceInstall = "daysSinceInstall"
Expand Down
10 changes: 8 additions & 2 deletions Core/PixelEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,10 @@ extension Pixel {

// Replacing secureVaultIsEnabledCheckedWhenEnabledAndBackgrounded with data protection check
case secureVaultIsEnabledCheckedWhenEnabledAndDataProtected


case secureVaultV4Migration
case secureVaultV4MigrationSkipped

// MARK: Ad Click Attribution pixels

case adClickAttributionDetected
Expand Down Expand Up @@ -1344,7 +1347,10 @@ extension Pixel.Event {
case .secureVaultFailedToOpenDatabaseError: return "m_secure-vault_error_failed-to-open-database"

case .secureVaultIsEnabledCheckedWhenEnabledAndDataProtected: return "m_secure-vault_is-enabled-checked_when-enabled-and-data-protected"


case .secureVaultV4Migration: return "m_secure-vault_v4-migration"
case .secureVaultV4MigrationSkipped: return "m_secure-vault_v4-migration-skipped"

// MARK: Ad Click Attribution pixels

case .adClickAttributionDetected: return "m_ad_click_detected"
Expand Down
1 change: 1 addition & 0 deletions Core/UserDefaultsPropertyWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public struct UserDefaultsWrapper<T> {
case autofillOnboardedUser = "com.duckduckgo.app.autofill.OnboardedUser"
case autofillSurveysCompleted = "com.duckduckgo.app.autofill.SurveysCompleted"
case autofillExtensionEnabled = "com.duckduckgo.app.autofill.ExtensionEnabled"
case autofillVaultMigrated = "com.duckduckgo.app.autofill.VaultMigrated"

case syncPromoBookmarksDismissed = "com.duckduckgo.app.sync.PromoBookmarksDismissed"
case syncPromoPasswordsDismissed = "com.duckduckgo.app.sync.PromoPasswordsDismissed"
Expand Down
Loading

0 comments on commit 529c099

Please sign in to comment.