diff --git a/.swiftlint.yml b/.swiftlint.yml index bb60ab08b..f66182e12 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,5 +1,6 @@ disabled_rules: - trailing_whitespace + - nesting line_length: warning: 150 @@ -18,4 +19,4 @@ type_name: error: 100 excluded: - - .build \ No newline at end of file + - .build diff --git a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift index 621f31d16..b67a2e4a4 100644 --- a/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift +++ b/Sources/BrowserServicesKit/Autofill/AutofillUserScript+SecureVault.swift @@ -134,30 +134,35 @@ extension AutofillUserScript { // MARK: - Requests - public struct IncomingCredentials { - + public struct IncomingCredentials: Equatable { + private enum Constants { static let credentialsKey = "credentials" static let usernameKey = "username" static let passwordKey = "password" + static let autogeneratedKey = "autogenerated" } let username: String? let password: String + let autogenerated: Bool - init(username: String?, password: String) { + init(username: String?, password: String, autogenerated: Bool = false) { self.username = username self.password = password + self.autogenerated = autogenerated } init?(autofillDictionary: [String: Any]) { - guard let credentialsDictionary = autofillDictionary[Constants.credentialsKey] as? [String: String], - let password = credentialsDictionary[Constants.passwordKey] else { + guard let credentialsDictionary = autofillDictionary[Constants.credentialsKey] as? [String: Any], + let password = credentialsDictionary[Constants.passwordKey] as? String else { return nil } // Usernames are optional, as the Autofill script can pass a generated password through without a corresponding username. - self.init(username: credentialsDictionary[Constants.usernameKey], password: password) + self.init(username: credentialsDictionary[Constants.usernameKey] as? String, + password: password, + autogenerated: (credentialsDictionary[Constants.autogeneratedKey] as? Bool) ?? false) } } @@ -172,6 +177,10 @@ extension AutofillUserScript { public let credentials: IncomingCredentials? public let creditCard: SecureVaultModels.CreditCard? + var hasAutogeneratedPassword: Bool { + return credentials?.autogenerated ?? false + } + init(dictionary: [String: Any]) { self.identity = .init(autofillDictionary: dictionary) self.creditCard = .init(autofillDictionary: dictionary) diff --git a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesCompilationTask.swift b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesCompilationTask.swift index 9b91d8551..6a6f1501e 100644 --- a/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesCompilationTask.swift +++ b/Sources/BrowserServicesKit/ContentBlocking/ContentBlockingRulesCompilationTask.swift @@ -28,7 +28,6 @@ extension ContentBlockerRulesManager { Encapsulates compilation steps for a single Task */ class CompilationTask { - // swiftlint:disable:next nesting typealias Completion = (_ success: Bool) -> Void let workQueue: DispatchQueue let rulesList: ContentBlockerRulesList diff --git a/Sources/BrowserServicesKit/Resources/duckduckgo-autofill b/Sources/BrowserServicesKit/Resources/duckduckgo-autofill index 4df6b625a..dd9165ab9 160000 --- a/Sources/BrowserServicesKit/Resources/duckduckgo-autofill +++ b/Sources/BrowserServicesKit/Resources/duckduckgo-autofill @@ -1 +1 @@ -Subproject commit 4df6b625a41d909793a1b89867d767e74477549f +Subproject commit dd9165ab903eb22894a67bafaa7365a31e208906 diff --git a/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift b/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift index ddec65a7d..e1872eda7 100644 --- a/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift +++ b/Sources/BrowserServicesKit/SecureVault/SecureVaultManager.swift @@ -31,6 +31,7 @@ public struct AutofillData { public let identity: SecureVaultModels.Identity? public let credentials: SecureVaultModels.WebsiteCredentials? public let creditCard: SecureVaultModels.CreditCard? + public let automaticallySavedCredentials: Bool } public protocol SecureVaultManagerDelegate: SecureVaultErrorReporting { @@ -39,14 +40,20 @@ public protocol SecureVaultManagerDelegate: SecureVaultErrorReporting { func secureVaultManager(_: SecureVaultManager, didAutofill type: AutofillType, withObjectId objectId: Int64) + // swiftlint:disable:next identifier_name func secureVaultManager(_: SecureVaultManager, didRequestAuthenticationWithCompletionHandler: @escaping (Bool) -> Void) + } public class SecureVaultManager { public weak var delegate: SecureVaultManagerDelegate? + + private let vault: SecureVault? - public init() { } + public init(vault: SecureVault? = nil) { + self.vault = vault + } } @@ -60,7 +67,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { [SecureVaultModels.CreditCard]) -> Void) { do { - let vault = try SecureVaultFactory.default.makeVault(errorReporter: self.delegate) + let vault = try self.vault ?? SecureVaultFactory.default.makeVault(errorReporter: self.delegate) let accounts = try vault.accountsFor(domain: domain) let identities = try vault.identities() let cards = try vault.creditCards() @@ -79,9 +86,14 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { /// Receives each of the types of data that the Autofill script has detected, and determines whether the user should be prompted to save them. /// This involves checking each proposed object to determine whether it already exists in the store. /// Currently, only one new type of data is presented to the user, but that decision is handled client-side so that it's easier to adapt in the future when multiple types are presented at once. - public func autofillUserScript(_: AutofillUserScript, didRequestStoreDataForDomain domain: String, data: AutofillUserScript.DetectedAutofillData) { + public func autofillUserScript(_: AutofillUserScript, + didRequestStoreDataForDomain domain: String, + data: AutofillUserScript.DetectedAutofillData) { do { - let dataToPrompt = try existingEntries(for: domain, autofillData: data) + let automaticallySavedCredentials = try storeOrUpdateAutogeneratedCredentials(domain: domain, autofillData: data) + try updateExistingAutogeneratedCredentialsWithSubmittedValues(domain: domain, autofillData: data) + + let dataToPrompt = try existingEntries(for: domain, autofillData: data, automaticallySavedCredentials: automaticallySavedCredentials) delegate?.secureVaultManager(self, promptUserToStoreAutofillData: dataToPrompt) } catch { os_log(.error, "Error storing data: %{public}@", error.localizedDescription) @@ -93,8 +105,8 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { completionHandler: @escaping ([SecureVaultModels.WebsiteAccount]) -> Void) { do { - completionHandler(try SecureVaultFactory.default.makeVault(errorReporter: self.delegate) - .accountsFor(domain: domain)) + let vault = try self.vault ?? SecureVaultFactory.default.makeVault(errorReporter: self.delegate) + completionHandler(try vault.accountsFor(domain: domain)) } catch { os_log(.error, "Error requesting accounts: %{public}@", error.localizedDescription) completionHandler([]) @@ -107,8 +119,8 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { completionHandler: @escaping (SecureVaultModels.WebsiteCredentials?) -> Void) { do { - completionHandler(try SecureVaultFactory.default.makeVault(errorReporter: self.delegate) - .websiteCredentialsFor(accountId: accountId)) + let vault = try self.vault ?? SecureVaultFactory.default.makeVault(errorReporter: self.delegate) + completionHandler(try vault.websiteCredentialsFor(accountId: accountId)) delegate?.secureVaultManager(self, didAutofill: .password, withObjectId: accountId) } catch { os_log(.error, "Error requesting credentials: %{public}@", error.localizedDescription) @@ -121,7 +133,8 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { didRequestCreditCardWithId creditCardId: Int64, completionHandler: @escaping (SecureVaultModels.CreditCard?) -> Void) { do { - let card = try SecureVaultFactory.default.makeVault(errorReporter: self.delegate).creditCardFor(id: creditCardId) + let vault = try self.vault ?? SecureVaultFactory.default.makeVault(errorReporter: self.delegate) + let card = try vault.creditCardFor(id: creditCardId) delegate?.secureVaultManager(self, didRequestAuthenticationWithCompletionHandler: { authenticated in if authenticated { @@ -142,8 +155,9 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { didRequestIdentityWithId identityId: Int64, completionHandler: @escaping (SecureVaultModels.Identity?) -> Void) { do { - completionHandler(try SecureVaultFactory.default.makeVault(errorReporter: self.delegate) - .identityFor(id: identityId)) + let vault = try self.vault ?? SecureVaultFactory.default.makeVault(errorReporter: self.delegate) + completionHandler(try vault.identityFor(id: identityId)) + delegate?.secureVaultManager(self, didAutofill: .identity, withObjectId: identityId) } catch { os_log(.error, "Error requesting identity: %{public}@", error.localizedDescription) @@ -151,14 +165,105 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { } } - func existingEntries(for domain: String, autofillData: AutofillUserScript.DetectedAutofillData) throws -> AutofillData { - let vault = try SecureVaultFactory.default.makeVault(errorReporter: self.delegate) + /// Stores autogenerated credentials sent by the AutofillUserScript, or updates an existing row in the database if credentials already exist. + /// The Secure Vault only stores one generated password for a domain, which is updated any time the user selects a new generated password. + func storeOrUpdateAutogeneratedCredentials(domain: String, autofillData: AutofillUserScript.DetectedAutofillData) throws -> Bool { + guard autofillData.hasAutogeneratedPassword, + let autogeneratedCredentials = autofillData.credentials, + !(autogeneratedCredentials.username?.isEmpty ?? true), + let passwordData = autogeneratedCredentials.password.data(using: .utf8) else { + os_log("Did not meet conditions for silently saving autogenerated credentials, returning early", log: .passwordManager) + return false + } + + let vault = try self.vault ?? SecureVaultFactory.default.makeVault(errorReporter: self.delegate) + let accounts = try vault.accountsFor(domain: domain) + + if accounts.contains(where: { account in account.username == autogeneratedCredentials.username }) { + os_log("Tried to automatically save credentials for which an account already exists, returning early", log: .passwordManager) + return false + } + + // As a precaution, check whether an account exists with the matching generated password _and_ a non-nil username. + // If so, then the user must have already saved the generated credentials and set a username. + + for account in accounts where !account.username.isEmpty { + if let accountID = account.id, + let credentialsForAccount = try vault.websiteCredentialsFor(accountId: accountID), + credentialsForAccount.password == passwordData, + account.username == autogeneratedCredentials.username ?? "" { + os_log("Tried to save autogenerated password but it already exists, returning early", log: .passwordManager) + return false + } + } + + let existingAccount = accounts.first(where: { $0.username == "" }) + var account = existingAccount ?? SecureVaultModels.WebsiteAccount(username: "", domain: domain) + + account.title = "Saved Password (\(domain))" + let generatedPassword = SecureVaultModels.WebsiteCredentials(account: account, password: passwordData) + + os_log("Saving autogenerated password", log: .passwordManager) + + try vault.storeWebsiteCredentials(generatedPassword) + + return true + } + + /// If credentials are sent via the AutofillUserScript, and there exists a credential row with empty username and matching password, then this function will update that credential row with the username. + /// This can happen if the user chooses to use a generated password when signing up for a service, then enters their own username and submits the form. + func updateExistingAutogeneratedCredentialsWithSubmittedValues(domain: String, autofillData: AutofillUserScript.DetectedAutofillData) throws { + let vault = try self.vault ?? SecureVaultFactory.default.makeVault(errorReporter: self.delegate) + let accounts = try vault.accountsFor(domain: domain) + + guard let autofillCredentials = autofillData.credentials, + let autofillCredentialsUsername = autofillCredentials.username, + var existingAccount = accounts.first(where: { $0.username == "" }), + let existingAccountID = existingAccount.id else { + return + } + + if accounts.contains(where: { $0.username == autofillCredentials.username }) { + os_log("ERROR: Tried to save generated credentials with an existing username, prompt the user to update instead.", log: .passwordManager) + return + } + + let existingCredentials = try vault.websiteCredentialsFor(accountId: existingAccountID) + + guard let existingPasswordData = existingCredentials?.password, + let autofillPasswordData = autofillCredentials.password.data(using: .utf8) else { + return + } + + // If true, then the existing generated password matches the credentials sent by the script, so update and save the difference. + if existingPasswordData == autofillPasswordData { + os_log("Found matching autogenerated credentials in Secure Vault, updating with username", log: .passwordManager) + + existingAccount.username = autofillCredentialsUsername + existingAccount.title = nil // Remove the "Saved Password" title so that the UI uses the default title format + + let credentialsToSave = SecureVaultModels.WebsiteCredentials(account: existingAccount, password: autofillPasswordData) + + try vault.storeWebsiteCredentials(credentialsToSave) + } + } + + func existingEntries(for domain: String, + autofillData: AutofillUserScript.DetectedAutofillData, + automaticallySavedCredentials: Bool) throws -> AutofillData { + let vault = try self.vault ?? SecureVaultFactory.default.makeVault(errorReporter: self.delegate) let proposedIdentity = try existingIdentity(with: autofillData, vault: vault) - let proposedCredentials = try existingCredentials(with: autofillData, domain: domain, vault: vault) + let proposedCredentials = try existingCredentials(with: autofillData, + domain: domain, + automaticallySavedCredentials: automaticallySavedCredentials, + vault: vault) let proposedCard = try existingPaymentMethod(with: autofillData, vault: vault) - return AutofillData(identity: proposedIdentity, credentials: proposedCredentials, creditCard: proposedCard) + return AutofillData(identity: proposedIdentity, + credentials: proposedCredentials, + creditCard: proposedCard, + automaticallySavedCredentials: automaticallySavedCredentials) } private func existingIdentity(with autofillData: AutofillUserScript.DetectedAutofillData, @@ -174,17 +279,23 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { private func existingCredentials(with autofillData: AutofillUserScript.DetectedAutofillData, domain: String, + automaticallySavedCredentials: Bool, vault: SecureVault) throws -> SecureVaultModels.WebsiteCredentials? { if let credentials = autofillData.credentials, let passwordData = credentials.password.data(using: .utf8) { if let account = try vault .accountsFor(domain: domain) - .first(where: { $0.username == credentials.username }) { + .first(where: { $0.username == credentials.username ?? "" }) { if let existingAccountID = account.id, let existingCredentials = try vault.websiteCredentialsFor(accountId: existingAccountID), existingCredentials.password == passwordData { - os_log("Found duplicate credentials, avoid prompting user", log: .passwordManager) - return nil + if automaticallySavedCredentials { + os_log("Found duplicate credentials which were just saved, notifying user", log: .passwordManager) + return SecureVaultModels.WebsiteCredentials(account: account, password: passwordData) + } else { + os_log("Found duplicate credentials which were previously saved, avoid notifying user", log: .passwordManager) + return nil + } } else { os_log("Found existing credentials to update", log: .passwordManager) return SecureVaultModels.WebsiteCredentials(account: account, password: passwordData) @@ -203,7 +314,7 @@ extension SecureVaultManager: AutofillSecureVaultDelegate { } private func existingPaymentMethod(with autofillData: AutofillUserScript.DetectedAutofillData, - vault: SecureVault) throws -> SecureVaultModels.CreditCard? { + vault: SecureVault) throws -> SecureVaultModels.CreditCard? { if let card = autofillData.creditCard, try vault.existingCardForAutofill(matching: card) == nil { os_log("Got new payment method to save", log: .passwordManager) return card diff --git a/Tests/BrowserServicesKitTests/SecureVault/MockVaultProviders.swift b/Tests/BrowserServicesKitTests/SecureVault/MockVaultProviders.swift index dfcd787bd..b65f62c9a 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/MockVaultProviders.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/MockVaultProviders.swift @@ -22,25 +22,27 @@ import Foundation internal class MockDatabaseProvider: SecureVaultDatabaseProvider { // swiftlint:disable identifier_name - var _accounts = [SecureVaultModels.WebsiteAccount]() - var _notes = [SecureVaultModels.Note]() - var _identities = [SecureVaultModels.Identity]() - var _creditCards = [SecureVaultModels.CreditCard]() + var _accounts = [SecureVaultModels.WebsiteAccount]() + var _notes = [SecureVaultModels.Note]() + var _identities = [Int64: SecureVaultModels.Identity]() + var _creditCards = [Int64: SecureVaultModels.CreditCard]() var _forDomain = [String]() - var _credentials: SecureVaultModels.WebsiteCredentials? - var _lastCredentials: SecureVaultModels.WebsiteCredentials? + var _credentialsDict = [Int64: SecureVaultModels.WebsiteCredentials]() var _note: SecureVaultModels.Note? - var _identity: SecureVaultModels.Identity? - var _creditCard: SecureVaultModels.CreditCard? // swiftlint:enable identifier_name func storeWebsiteCredentials(_ credentials: SecureVaultModels.WebsiteCredentials) throws -> Int64 { - _lastCredentials = credentials - return _lastCredentials?.account.id ?? -1 + if let accountID = credentials.account.id { + _credentialsDict[accountID] = credentials + return accountID + } else { + _credentialsDict[-1] = credentials + return -1 + } } func websiteCredentialsForAccountId(_ accountId: Int64) throws -> SecureVaultModels.WebsiteCredentials? { - return _credentials + return _credentialsDict[accountId] } func websiteAccountsForDomain(_ domain: String) throws -> [SecureVaultModels.WebsiteAccount] { @@ -74,37 +76,45 @@ internal class MockDatabaseProvider: SecureVaultDatabaseProvider { } func identities() throws -> [SecureVaultModels.Identity] { - return _identities + return Array(_identities.values) } func identityForIdentityId(_ identityId: Int64) throws -> SecureVaultModels.Identity? { - return _identity + return _identities[identityId] } func storeIdentity(_ identity: SecureVaultModels.Identity) throws -> Int64 { - _identity = identity - return identity.id ?? -1 + if let identityID = identity.id { + _identities[identityID] = identity + return identityID + } else { + return -1 + } } func deleteIdentityForIdentityId(_ identityId: Int64) throws { - self._identities = self._identities.filter { $0.id != identityId } + _identities.removeValue(forKey: identityId) } func creditCards() throws -> [SecureVaultModels.CreditCard] { - return _creditCards + return Array(_creditCards.values) } func creditCardForCardId(_ cardId: Int64) throws -> SecureVaultModels.CreditCard? { - return _creditCard + return _creditCards[cardId] } func storeCreditCard(_ creditCard: SecureVaultModels.CreditCard) throws -> Int64 { - _creditCard = creditCard - return creditCard.id ?? -1 + if let cardID = creditCard.id { + _creditCards[cardID] = creditCard + return cardID + } else { + return -1 + } } func deleteCreditCardForCreditCardId(_ cardId: Int64) throws { - self._creditCards = self._creditCards.filter { $0.id != cardId } + _creditCards.removeValue(forKey: cardId) } } @@ -137,7 +147,7 @@ internal class MockCryptoProvider: SecureVaultCryptoProvider { func encrypt(_ data: Data, withKey key: Data) throws -> Data { _lastDataToEncrypt = data _lastKey = key - return Data() + return data } func decrypt(_ data: Data, withKey key: Data) throws -> Data { @@ -153,6 +163,34 @@ internal class MockCryptoProvider: SecureVaultCryptoProvider { } +internal class NoOpCryptoProvider: SecureVaultCryptoProvider { + + func generateSecretKey() throws -> Data { + return Data() + } + + func generatePassword() throws -> Data { + return Data() + } + + func deriveKeyFromPassword(_ password: Data) throws -> Data { + return password + } + + func generateNonce() throws -> Data { + return Data() + } + + func encrypt(_ data: Data, withKey key: Data) throws -> Data { + return data + } + + func decrypt(_ data: Data, withKey key: Data) throws -> Data { + return data + } + +} + internal class MockKeystoreProvider: SecureVaultKeyStoreProvider { // swiftlint:disable identifier_name diff --git a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultManagerTests.swift b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultManagerTests.swift new file mode 100644 index 000000000..eda7372a6 --- /dev/null +++ b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultManagerTests.swift @@ -0,0 +1,239 @@ +// +// SecureVaultManagerTests.swift +// +// Copyright © 2022 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 XCTest +@testable import BrowserServicesKit + +class SecureVaultManagerTests: XCTestCase { + + private var mockCryptoProvider = NoOpCryptoProvider() + private var mockDatabaseProvider = MockDatabaseProvider() + private var mockKeystoreProvider = MockKeystoreProvider() + + private let mockAutofillUserScript: AutofillUserScript = { + let embeddedConfig = + """ + { + "features": { + "autofill": { + "status": "enabled", + "exceptions": [] + } + }, + "unprotectedTemporary": [] + } + """.data(using: .utf8)! + let privacyConfig = AutofillTestHelper.preparePrivacyConfig(embeddedConfig: embeddedConfig) + let properties = ContentScopeProperties(gpcEnabled: false, sessionKey: "1234") + let sourceProvider = DefaultAutofillSourceProvider(privacyConfigurationManager: privacyConfig, + properties: properties) + return AutofillUserScript(scriptSourceProvider: sourceProvider, encrypter: MockEncrypter(), hostProvider: SecurityOriginHostProvider()) + }() + + private var testVault: SecureVault! + private var secureVaultManagerDelegate: MockSecureVaultManagerDelegate! + private var manager: SecureVaultManager! + + override func setUp() { + super.setUp() + + mockKeystoreProvider._generatedPassword = "generated".data(using: .utf8) + mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8) + + let providers = SecureVaultProviders(crypto: mockCryptoProvider, database: mockDatabaseProvider, keystore: mockKeystoreProvider) + + self.testVault = DefaultSecureVault(authExpiry: 30, providers: providers) + self.secureVaultManagerDelegate = MockSecureVaultManagerDelegate() + self.manager = SecureVaultManager(vault: self.testVault) + self.manager.delegate = secureVaultManagerDelegate + } + + func testWhenGettingExistingEntries_AndNoAutofillDataWasProvided_AndNoEntriesExist_ThenReturnValueIsNil() throws { + let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: nil, creditCard: nil) + let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData, automaticallySavedCredentials: false) + + XCTAssertNil(entries.credentials) + XCTAssertNil(entries.identity) + XCTAssertNil(entries.creditCard) + } + + func testWhenGettingExistingEntries_AndAutofillCreditCardWasProvided_AndNoMatchingCreditCardExists_ThenReturnValueIncludesCard() throws { + let card = paymentMethod(cardNumber: "5555555555555557", cardholderName: "Name", cvv: "123", month: 1, year: 2022) + + let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: nil, creditCard: card) + let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData, automaticallySavedCredentials: false) + + XCTAssertNil(entries.credentials) + XCTAssertNil(entries.identity) + XCTAssertNotNil(entries.creditCard) + XCTAssertTrue(entries.creditCard!.hasAutofillEquality(comparedTo: card)) + } + + func testWhenGettingExistingEntries_AndAutofillCreditCardWasProvided_AndMatchingCreditCardExists_ThenReturnValueIsNil() throws { + let card = paymentMethod(id: 1, cardNumber: "5555555555555557", cardholderName: "Name", cvv: "123", month: 1, year: 2022) + try self.testVault.storeCreditCard(card) + + let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: nil, creditCard: card) + let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData, automaticallySavedCredentials: false) + + XCTAssertNil(entries.credentials) + XCTAssertNil(entries.identity) + XCTAssertNil(entries.creditCard) + } + + func testWhenGettingExistingEntries_AndAutofillIdentityWasProvided_AndNoMatchingIdentityExists_ThenReturnValueIncludesIdentity() throws { + let identity = identity(name: ("First", "Middle", "Last"), addressStreet: "Address Street") + + let autofillData = AutofillUserScript.DetectedAutofillData(identity: identity, credentials: nil, creditCard: nil) + let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData, automaticallySavedCredentials: false) + + XCTAssertNil(entries.credentials) + XCTAssertNil(entries.creditCard) + XCTAssertNotNil(entries.identity) + XCTAssertTrue(entries.identity!.hasAutofillEquality(comparedTo: identity)) + } + + func testWhenGettingExistingEntries_AndAutofillIdentityWasProvided_AndMatchingIdentityExists_ThenReturnValueIsNil() throws { + let identity = identity(id: 1, name: ("First", "Middle", "Last"), addressStreet: "Address Street") + try self.testVault.storeIdentity(identity) + + let autofillData = AutofillUserScript.DetectedAutofillData(identity: identity, credentials: nil, creditCard: nil) + let entries = try manager.existingEntries(for: "domain.com", autofillData: autofillData, automaticallySavedCredentials: false) + + XCTAssertNil(entries.credentials) + XCTAssertNil(entries.identity) + XCTAssertNil(entries.creditCard) + } + + // MARK: - AutofillSecureVaultDelegate Tests + + func testWhenRequestingToStoreCredentials_AndCredentialsDoNotExist_ThenTheDelegateIsPromptedToStoreAutofillData() { + let incomingCredentials = AutofillUserScript.IncomingCredentials(username: "username", password: "password", autogenerated: false) + let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil) + + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData) + manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "domain.com", data: autofillData) + XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) + XCTAssertEqual(incomingCredentials, autofillData.credentials) + } + + func testWhenRequestingToStoreCredentials_AndCredentialsAreGenerated_AndNoCredentialsAlreadyExist_ThenTheDelegateIsPromptedToStoreAutofillData() throws { + let incomingCredentials = AutofillUserScript.IncomingCredentials(username: nil, password: "password", autogenerated: true) + let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil) + + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData) + manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: "domain.com", data: autofillData) + XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) + + XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.account.username, "") + XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.account.domain, "domain.com") + XCTAssertEqual(secureVaultManagerDelegate.promptedAutofillData?.credentials?.password, "password".data(using: .utf8)!) + } + + func testWhenRequestingToStoreCredentials_AndCredentialsAreAutoGenerated_AndCredentialsAlreadyExist_ThenPromptedAutofillDataIsEmpty() throws { + let domain = "domain.com" + let account = SecureVaultModels.WebsiteAccount(id: 1, title: nil, username: "", domain: domain, created: Date(), lastUpdated: Date()) + self.mockDatabaseProvider._accounts = [account] + let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: "password".data(using: .utf8)!) + try self.testVault.storeWebsiteCredentials(credentials) + + let incomingCredentials = AutofillUserScript.IncomingCredentials(username: "", password: "password", autogenerated: true) + let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil) + + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData) + manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: domain, data: autofillData) + XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData?.credentials) + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData?.creditCard) + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData?.identity) + } + + func testWhenRequestingToStoreCredentials_AndCredentialsAreNotAutoGenerated_AndCredentialsAlreadyExist_ThenPromptedAutofillDataIsEmpty() throws { + let domain = "domain.com" + let account = SecureVaultModels.WebsiteAccount(id: 1, title: nil, username: "username", domain: domain, created: Date(), lastUpdated: Date()) + self.mockDatabaseProvider._accounts = [account] + let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: "password".data(using: .utf8)!) + try self.testVault.storeWebsiteCredentials(credentials) + + let incomingCredentials = AutofillUserScript.IncomingCredentials(username: "username", password: "password", autogenerated: false) + let autofillData = AutofillUserScript.DetectedAutofillData(identity: nil, credentials: incomingCredentials, creditCard: nil) + + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData) + manager.autofillUserScript(mockAutofillUserScript, didRequestStoreDataForDomain: domain, data: autofillData) + XCTAssertNotNil(secureVaultManagerDelegate.promptedAutofillData) + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData?.credentials) + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData?.creditCard) + XCTAssertNil(secureVaultManagerDelegate.promptedAutofillData?.identity) + } + + // MARK: - Test Utilities + + private func identity(id: Int64? = nil, name: (String, String, String), addressStreet: String?) -> SecureVaultModels.Identity { + return SecureVaultModels.Identity(id: id, + title: nil, + created: Date(), + lastUpdated: Date(), + firstName: name.0, + middleName: name.1, + lastName: name.2, + birthdayDay: nil, + birthdayMonth: nil, + birthdayYear: nil, + addressStreet: addressStreet, + addressStreet2: nil, + addressCity: nil, + addressProvince: nil, + addressPostalCode: nil, + addressCountryCode: nil, + homePhone: nil, + mobilePhone: nil, + emailAddress: nil) + } + + private func paymentMethod(id: Int64? = nil, + cardNumber: String, + cardholderName: String, + cvv: String, + month: Int, + year: Int) -> SecureVaultModels.CreditCard { + return SecureVaultModels.CreditCard(id: id, + title: nil, + cardNumber: cardNumber, + cardholderName: cardholderName, + cardSecurityCode: cvv, + expirationMonth: month, + expirationYear: year) + } + +} + +private class MockSecureVaultManagerDelegate: SecureVaultManagerDelegate { + + private(set) var promptedAutofillData: AutofillData? + + func secureVaultManager(_: SecureVaultManager, promptUserToStoreAutofillData data: AutofillData) { + self.promptedAutofillData = data + } + + func secureVaultManager(_: SecureVaultManager, didAutofill type: AutofillType, withObjectId objectId: Int64) {} + + func secureVaultManager(_: SecureVaultManager, didRequestAuthenticationWithCompletionHandler: @escaping (Bool) -> Void) {} + + func secureVaultInitFailed(_ error: SecureVaultError) {} + +} diff --git a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultTests.swift b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultTests.swift index d9127e926..e0e151295 100644 --- a/Tests/BrowserServicesKitTests/SecureVault/SecureVaultTests.swift +++ b/Tests/BrowserServicesKitTests/SecureVault/SecureVaultTests.swift @@ -73,13 +73,20 @@ class SecureVaultTests: XCTestCase { } func testWhenDeletingCredentialsForAccount_ThenDatabaseCalled() throws { + mockKeystoreProvider._generatedPassword = "generated".data(using: .utf8)! + mockCryptoProvider._derivedKey = "derived".data(using: .utf8)! + mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8)! + mockCryptoProvider._decryptedData = "decrypted".data(using: .utf8)! + let account = SecureVaultModels.WebsiteAccount(id: 1, title: "Title", username: "test@duck.com", domain: "example.com", created: Date(), lastUpdated: Date()) - mockDatabaseProvider._credentials = SecureVaultModels.WebsiteCredentials(account: account, password: "password".data(using: .utf8)!) + let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: "password".data(using: .utf8)!) + + try testVault.storeWebsiteCredentials(credentials) mockDatabaseProvider._accounts = [account] XCTAssertEqual("example.com", mockDatabaseProvider._accounts[0].domain) @@ -134,7 +141,6 @@ class SecureVaultTests: XCTestCase { } func testWhenStoringWebsiteCredentials_ThenThePasswordIsEncryptedWithL2Key() throws { - mockKeystoreProvider._generatedPassword = "generated".data(using: .utf8)! mockCryptoProvider._derivedKey = "derived".data(using: .utf8)! mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8)! @@ -147,24 +153,27 @@ class SecureVaultTests: XCTestCase { try testVault.storeWebsiteCredentials(credentials) - XCTAssertNotNil(mockDatabaseProvider._lastCredentials) + XCTAssertNotNil(mockDatabaseProvider._credentialsDict.first) XCTAssertEqual(mockCryptoProvider._lastDataToEncrypt, passwordToEncrypt) } func testWhenCredentialsAreRetrievedUsingGeneratedPassword_ThenTheyAreDecrypted() throws { - let password = "password".data(using: .utf8)! - let account = SecureVaultModels.WebsiteAccount(username: "test@duck.com", domain: "example.com") - mockDatabaseProvider._credentials = SecureVaultModels.WebsiteCredentials(account: account, password: password) + let account = SecureVaultModels.WebsiteAccount(id: 1, username: "test@duck.com", domain: "example.com", created: Date(), lastUpdated: Date()) + let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: password) + self.mockDatabaseProvider._accounts = [account] + mockCryptoProvider._decryptedData = "decrypted".data(using: .utf8) mockKeystoreProvider._generatedPassword = "generated".data(using: .utf8) mockCryptoProvider._derivedKey = "derived".data(using: .utf8) mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8) - - let credentials = try testVault.websiteCredentialsFor(accountId: 1) - XCTAssertNotNil(credentials) - XCTAssertNotNil(credentials?.password) + + try testVault.storeWebsiteCredentials(credentials) + + let fetchedCredentials = try testVault.websiteCredentialsFor(accountId: 1) + XCTAssertNotNil(fetchedCredentials) + XCTAssertNotNil(fetchedCredentials?.password) XCTAssertEqual(mockCryptoProvider._lastDataToDecrypt, password) @@ -173,25 +182,27 @@ class SecureVaultTests: XCTestCase { func testWhenCredentialsAreRetrievedUsingUserPassword_ThenTheyAreDecrypted() throws { let userPassword = "userPassword".data(using: .utf8)! let password = "password".data(using: .utf8)! - let account = SecureVaultModels.WebsiteAccount(username: "test@duck.com", domain: "example.com") - mockDatabaseProvider._credentials = SecureVaultModels.WebsiteCredentials(account: account, password: password) + let account = SecureVaultModels.WebsiteAccount(id: 1, username: "test@duck.com", domain: "example.com", created: Date(), lastUpdated: Date()) + let credentials = SecureVaultModels.WebsiteCredentials(account: account, password: password) + self.mockDatabaseProvider._accounts = [account] + mockCryptoProvider._decryptedData = "decrypted".data(using: .utf8) mockCryptoProvider._derivedKey = "derived".data(using: .utf8) mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8) + + _ = try testVault.authWith(password: userPassword) + try testVault.storeWebsiteCredentials(credentials) - let credentials = try testVault.authWith(password: userPassword).websiteCredentialsFor(accountId: 1) + let fetchedCredentials = try testVault.authWith(password: userPassword).websiteCredentialsFor(accountId: 1) - XCTAssertNotNil(credentials) - XCTAssertNotNil(credentials?.password) + XCTAssertNotNil(fetchedCredentials) + XCTAssertNotNil(fetchedCredentials?.password) XCTAssertEqual(mockCryptoProvider._lastDataToDecrypt, password) } func testWhenCredentialsAreRetrievedUsingExpiredUserPassword_ThenErrorIsThrown() throws { let userPassword = "userPassword".data(using: .utf8)! - let password = "password".data(using: .utf8)! - let account = SecureVaultModels.WebsiteAccount(username: "test@duck.com", domain: "example.com") - mockDatabaseProvider._credentials = SecureVaultModels.WebsiteCredentials(account: account, password: password) mockCryptoProvider._decryptedData = "decrypted".data(using: .utf8) mockCryptoProvider._derivedKey = "derived".data(using: .utf8) mockKeystoreProvider._encryptedL2Key = "encryptedL2Key".data(using: .utf8)