From 4e4e448fcb7e217346bf106b6c8ca7cc78667007 Mon Sep 17 00:00:00 2001 From: Anya Mallon Date: Fri, 20 Dec 2024 13:57:53 +0100 Subject: [PATCH] Fallback migrator for users impacted by enabling the credential before the app could perform migration on app version 7.149.0 --- Core/AutofillVaultKeychainMigrator.swift | 102 +++++++++++++++++++++++ DuckDuckGo.xcodeproj/project.pbxproj | 4 + DuckDuckGo/AutofillUsageMonitor.swift | 4 + 3 files changed, 110 insertions(+) create mode 100644 Core/AutofillVaultKeychainMigrator.swift diff --git a/Core/AutofillVaultKeychainMigrator.swift b/Core/AutofillVaultKeychainMigrator.swift new file mode 100644 index 0000000000..ac18bbde89 --- /dev/null +++ b/Core/AutofillVaultKeychainMigrator.swift @@ -0,0 +1,102 @@ +// +// 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 + +public struct AutofillVaultKeychainMigrator { + + public init() {} + + public func resetVaultMigrationIfRequired(fileManager: FileManager = FileManager.default) { + let originalVaultLocation = DefaultAutofillDatabaseProvider.defaultDatabaseURL() + let sharedVaultLocation = DefaultAutofillDatabaseProvider.defaultSharedDatabaseURL() + + // only care about users who have have both the original and migrated vaults + guard fileManager.fileExists(atPath: originalVaultLocation.path), + fileManager.fileExists(atPath: sharedVaultLocation.path) else { + return + } + + let hasV4Items = hasKeychainItemsMatching(serviceName: "DuckDuckGo Secure Vault v4") + + guard hasV4Items else { + return + } + + let hasV3Items = hasKeychainItemsMatching(serviceName: "DuckDuckGo Secure Vault v3") + let hasV2Items = hasKeychainItemsMatching(serviceName: "DuckDuckGo Secure Vault v2") + let hasV1Items = hasKeychainItemsMatching(serviceName: "DuckDuckGo Secure Vault") + + // Only continue if there are original keychain items to migrate from + guard hasV1Items || hasV2Items || hasV3Items else { + return + } + + deleteKeychainItems(matching: "DuckDuckGo Secure Vault v4") + let backupFilePath = sharedVaultLocation.appendingPathExtension("bak") + do { + // Creating a backup of the migrated file + try fileManager.moveItem(at: sharedVaultLocation, to: backupFilePath) + Logger.autofill.info("Move migrated file to backup \(backupFilePath.path)") + } catch { + Logger.autofill.error("Failed to create backup of migrated file: \(error.localizedDescription)") + return + } + } + + private 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 + } + + private 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)") + } + } +} diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9017f26c33..96d0432a39 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -951,6 +951,7 @@ C1CDA31E2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1CDA31D2AFBF811006D1476 /* AutofillNeverPromptWebsitesManagerTests.swift */; }; C1D21E2D293A5965006E5A05 /* AutofillLoginSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D21E2C293A5965006E5A05 /* AutofillLoginSession.swift */; }; C1D21E2F293A599C006E5A05 /* AutofillLoginSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D21E2E293A599C006E5A05 /* AutofillLoginSessionTests.swift */; }; + C1E12B7A2D14B86A0079AD8F /* AutofillVaultKeychainMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E12B792D14B86A0079AD8F /* AutofillVaultKeychainMigrator.swift */; }; C1E42C7B2C5CD8AE00509204 /* AutofillCredentialsDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1E42C7A2C5CD8AD00509204 /* AutofillCredentialsDebugViewController.swift */; }; C1E4E9A62D0861AD00AA39AF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1E4E9A42D0861AD00AA39AF /* InfoPlist.strings */; }; C1E4E9A92D0861AD00AA39AF /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C1E4E9A72D0861AD00AA39AF /* Localizable.strings */; }; @@ -2833,6 +2834,7 @@ C1D21E2E293A599C006E5A05 /* AutofillLoginSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillLoginSessionTests.swift; sourceTree = ""; }; C1DCF3502D0862330055F8B0 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; C1DCF3512D0862330055F8B0 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + C1E12B792D14B86A0079AD8F /* AutofillVaultKeychainMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillVaultKeychainMigrator.swift; sourceTree = ""; }; C1E42C7A2C5CD8AD00509204 /* AutofillCredentialsDebugViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AutofillCredentialsDebugViewController.swift; sourceTree = ""; }; C1E490582D08646400F86C5A /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/InfoPlist.strings; sourceTree = ""; }; C1E490592D08646400F86C5A /* sl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Localizable.strings; sourceTree = ""; }; @@ -5474,6 +5476,7 @@ C19D90D02CFE3A7F00D17DF3 /* AutofillLoginListSectionType.swift */, C1CAAA992CFCAD3E00C37EE6 /* AutofillLoginItem.swift */, C1CAAA9B2CFCB39800C37EE6 /* AutofillLoginListSorting.swift */, + C1E12B792D14B86A0079AD8F /* AutofillVaultKeychainMigrator.swift */, ); name = Autofill; sourceTree = ""; @@ -8750,6 +8753,7 @@ B652DF0D287C2A6300C12A9C /* PrivacyFeatures.swift in Sources */, F10E522D1E946F8800CE1253 /* NSAttributedStringExtension.swift in Sources */, 9F678B892C9BAA4800CA0E19 /* Debouncer.swift in Sources */, + C1E12B7A2D14B86A0079AD8F /* AutofillVaultKeychainMigrator.swift in Sources */, 85AB84C32CF624D8007E679F /* HTTPCookieExtension.swift in Sources */, 9887DC252354D2AA005C85F5 /* Database.swift in Sources */, F143C3171E4A99D200CFDE3A /* AppURLs.swift in Sources */, diff --git a/DuckDuckGo/AutofillUsageMonitor.swift b/DuckDuckGo/AutofillUsageMonitor.swift index 6123c43180..cd4c04d71c 100644 --- a/DuckDuckGo/AutofillUsageMonitor.swift +++ b/DuckDuckGo/AutofillUsageMonitor.swift @@ -35,6 +35,10 @@ final class AutofillUsageMonitor { init() { NotificationCenter.default.addObserver(self, selector: #selector(didReceiveSaveEvent), name: .autofillSaveEvent, object: nil) + if autofillExtensionEnabled != nil { + AutofillVaultKeychainMigrator().resetVaultMigrationIfRequired() + } + ASCredentialIdentityStore.shared.getState({ [weak self] state in if state.isEnabled { if self?.autofillExtensionEnabled == nil {