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

Adds new debug screen to view raw credentials data #3335

Merged
merged 1 commit into from
Oct 7, 2024
Merged
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
20 changes: 20 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2749,6 +2749,10 @@
BDCB66D92C7CE1A700E8ABC9 /* VPNFeedbackFormViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */; };
BDE981D92BBD10D600645880 /* vpn-dark-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC82BBC821100EF3735 /* vpn-dark-mode.json */; };
BDE981DA2BBD10D600645880 /* vpn-light-mode.json in Resources */ = {isa = PBXBuildFile; fileRef = BD384AC72BBC821100EF3735 /* vpn-light-mode.json */; };
C10529432C9CC18B0041E502 /* AutofillCredentialsDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10529422C9CC18B0041E502 /* AutofillCredentialsDebugView.swift */; };
C10529442C9CC18B0041E502 /* AutofillCredentialsDebugView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10529422C9CC18B0041E502 /* AutofillCredentialsDebugView.swift */; };
C10529462C9F456C0041E502 /* AutofillCredentialsDebugViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10529452C9F456C0041E502 /* AutofillCredentialsDebugViewModel.swift */; };
C10529472C9F456C0041E502 /* AutofillCredentialsDebugViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C10529452C9F456C0041E502 /* AutofillCredentialsDebugViewModel.swift */; };
C1372EF42BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; };
C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */; };
C13909EF2B85FD4E001626ED /* AutofillActionExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */; };
Expand Down Expand Up @@ -4555,6 +4559,8 @@
BDBA859B2C5D25A300BC54F5 /* VPNFeedbackFormViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModel.swift; sourceTree = "<group>"; };
BDBA859E2C5D25B700BC54F5 /* VPNMetadataCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNMetadataCollector.swift; sourceTree = "<group>"; };
BDCB66D72C7CE1A600E8ABC9 /* VPNFeedbackFormViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNFeedbackFormViewModelTests.swift; sourceTree = "<group>"; };
C10529422C9CC18B0041E502 /* AutofillCredentialsDebugView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillCredentialsDebugView.swift; sourceTree = "<group>"; };
C10529452C9F456C0041E502 /* AutofillCredentialsDebugViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillCredentialsDebugViewModel.swift; sourceTree = "<group>"; };
C1372EF32BBC5BAD003F8793 /* SecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureTextField.swift; sourceTree = "<group>"; };
C13909EE2B85FD4E001626ED /* AutofillActionExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillActionExecutor.swift; sourceTree = "<group>"; };
C13909F32B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutofillDeleteAllPasswordsExecutorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -6607,6 +6613,7 @@
7B1E819A27C8874900FF0E60 /* Autofill */ = {
isa = PBXGroup;
children = (
C10529482C9F45720041E502 /* Debug */,
7B1E819B27C8874900FF0E60 /* ContentOverlayPopover.swift */,
7B1E819C27C8874900FF0E60 /* ContentOverlay.storyboard */,
7B1E819D27C8874900FF0E60 /* ContentOverlayViewController.swift */,
Expand Down Expand Up @@ -8986,6 +8993,15 @@
path = Assets;
sourceTree = "<group>";
};
C10529482C9F45720041E502 /* Debug */ = {
isa = PBXGroup;
children = (
C10529422C9CC18B0041E502 /* AutofillCredentialsDebugView.swift */,
C10529452C9F456C0041E502 /* AutofillCredentialsDebugViewModel.swift */,
);
path = Debug;
sourceTree = "<group>";
};
C13909F22B85FD60001626ED /* Autofill */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -10725,6 +10741,7 @@
3706FAF7293F65D500E42796 /* FireproofDomainsViewController.swift in Sources */,
F18826852BBEE31700D9AC4F /* PixelKit+Assertion.swift in Sources */,
4BF0E5062AD2551A00FFEC9E /* NetworkProtectionPixelEvent.swift in Sources */,
C10529442C9CC18B0041E502 /* AutofillCredentialsDebugView.swift in Sources */,
3706FAF8293F65D500E42796 /* URLEventHandler.swift in Sources */,
9FBD84742BB3E15D00220859 /* InstallationAttributionPixelHandler.swift in Sources */,
BBFB72802C48047C0088884C /* SortBookmarksViewModel.swift in Sources */,
Expand Down Expand Up @@ -11140,6 +11157,7 @@
567A23D52C81E2180010F66C /* ContextualDaxDialogsFactory.swift in Sources */,
3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */,
1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */,
C10529472C9F456C0041E502 /* AutofillCredentialsDebugViewModel.swift in Sources */,
7B7F5D252C52725A00826256 /* AddExcludedDomainButtonsView.swift in Sources */,
1D220BFD2B87AACF00F8BBC6 /* PrivacyProtectionStatus.swift in Sources */,
3706FBF8293F65D500E42796 /* TabBarFooter.swift in Sources */,
Expand Down Expand Up @@ -12461,6 +12479,7 @@
B647EFBB2922584B00BA628D /* AdClickAttributionTabExtension.swift in Sources */,
B677FC542B064A9C0099EB04 /* DataImportViewModel.swift in Sources */,
4B980E212817604000282EE1 /* NSNotificationName+Debug.swift in Sources */,
C10529432C9CC18B0041E502 /* AutofillCredentialsDebugView.swift in Sources */,
B690152C2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */,
31F7F2A6288AD2CA001C0D64 /* NavigationBarBadgeAnimationView.swift in Sources */,
AAC5E4F125D6BF10007F5990 /* AddressBarButton.swift in Sources */,
Expand Down Expand Up @@ -12582,6 +12601,7 @@
B63D466925BEB6C200874977 /* WKWebView+SessionState.swift in Sources */,
B6F1B0222BCE5658005E863C /* BrokenSiteInfoTabExtension.swift in Sources */,
3199AF7F2C80734A003AEBDC /* TabModalManageable.swift in Sources */,
C10529462C9F456C0041E502 /* AutofillCredentialsDebugViewModel.swift in Sources */,
4B4D60C02A0C848D00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */,
4B723E1226B0006E00E14D75 /* DataImport.swift in Sources */,
7BE146072A6A83C700C313B8 /* NetworkProtectionDebugMenu.swift in Sources */,
Expand Down
64 changes: 64 additions & 0 deletions DuckDuckGo/Autofill/Debug/AutofillCredentialsDebugView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// AutofillCredentialsDebugView.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 BrowserServicesKit
import Common
import SwiftUI

@available(macOS 13.5, *)
struct AutofillCredentialsDebugView: ModalView {

@ObservedObject private var model: AutofillCredentialsDebugViewModel
@State private var sortOrder = [KeyPathComparator(\AutofillCredentialsDebugViewModel.DisplayCredentials.accountId)]

init(model: AutofillCredentialsDebugViewModel = AutofillCredentialsDebugViewModel()) {
self.model = model
}

var body: some View {
Table(model.credentials, sortOrder: $sortOrder) {
TableColumn(Text(verbatim: "Id"), value: \.accountId) { selectableText($0.accountId) }
TableColumn(Text(verbatim: "Website URL"), value: \.websiteUrl) { selectableText($0.websiteUrl) }
TableColumn(Text(verbatim: "Domain"), value: \.domain) { selectableText($0.domain) }
TableColumn(Text(verbatim: "Username"), value: \.username) { selectableText($0.username) }
TableColumn(Text(verbatim: "Password"), value: \.displayPassword) { selectableText($0.displayPassword) }
TableColumn(Text(verbatim: "Notes"), value: \.notes) { selectableText($0.notes) }
TableColumn(Text(verbatim: "Created"), value: \.created) { selectableText($0.created) }
TableColumn(Text(verbatim: "Last Updated"), value: \.lastUpdated) { selectableText($0.lastUpdated) }
TableColumn(Text(verbatim: "Last Used"), value: \.lastUsed) { selectableText($0.lastUsed) }
TableColumn(Text(verbatim: "Signature"), value: \.signature) { selectableText($0.signature) }
}
.onChange(of: sortOrder) { _ in
applySort()
}
}

private func selectableText(_ content: String) -> some View {
Text(content)
.textSelection(.enabled)
}

private func applySort() {
model.credentials.sort(using: sortOrder)
}
}

@available(macOS 13.5, *)
#Preview {
AutofillCredentialsDebugView(model: AutofillCredentialsDebugViewModel())
}
150 changes: 150 additions & 0 deletions DuckDuckGo/Autofill/Debug/AutofillCredentialsDebugViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// AutofillCredentialsDebugViewModel.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 BrowserServicesKit
import Common
import os.log

final class AutofillCredentialsDebugViewModel: ObservableObject {

struct DisplayCredentials: Identifiable {

let tld: TLD
let autofillDomainNameUrlMatcher: AutofillDomainNameUrlMatcher
var credential: SecureVaultModels.WebsiteCredentials

var id = UUID()

var accountId: String {
credential.account.id ?? ""
}

var accountTitle: String {
credential.account.title ?? ""
}

var displayTitle: String {
credential.account.name(tld: tld, autofillDomainNameUrlMatcher: autofillDomainNameUrlMatcher)
}

var websiteUrl: String {
credential.account.domain ?? ""
}

var domain: String {
guard let url = credential.account.domain,
let urlComponents = autofillDomainNameUrlMatcher.normalizeSchemeForAutofill(url),
let domain = urlComponents.eTLDplus1(tld: tld) ?? urlComponents.host else {
return ""
}
return domain
}

var username: String {
credential.account.username ?? ""
}

var displayPassword: String {
return credential.password.flatMap { String(data: $0, encoding: .utf8) } ?? "FAILED TO DECODE PW"
}

var notes: String {
credential.account.notes ?? ""
}

var created: String {
"\(credential.account.created)"
}

var lastUpdated: String {
"\(credential.account.lastUpdated)"
}

var lastUsed: String {
credential.account.lastUsed != nil ? "\(credential.account.lastUsed!)" : ""
}

var signature: String {
credential.account.signature ?? ""
}
}

private let tld: TLD = ContentBlocking.shared.tld
private let autofillDomainNameUrlMatcher: AutofillDomainNameUrlMatcher = AutofillDomainNameUrlMatcher()
private var userAuthenticator: UserAuthenticating

@Published var credentials: [DisplayCredentials] = []

init(userAuthenticator: UserAuthenticating = DeviceAuthenticator.shared) {
self.userAuthenticator = userAuthenticator
beginAuthentication()
}

private func beginAuthentication() {
userAuthenticator.authenticateUser(reason: .viewAllCredentials) { [weak self] authenticationResult in
guard let self = self else {
return
}

if authenticationResult.authenticated {
self.credentials = self.loadCredentials()
}
}
}

private func loadCredentials() -> [DisplayCredentials] {
do {
let secureVault = try AutofillSecureVaultFactory.makeVault(reporter: SecureVaultReporter.shared)
let accounts = try secureVault.accounts()
var credentials: [DisplayCredentials] = []
var accountsFailedToLoad: [String?] = []

for account in accounts {
guard let accountId = account.id,
let accountIdInt = Int64(accountId),
let credential = try secureVault.websiteCredentialsFor(accountId: accountIdInt) else {
accountsFailedToLoad.append(account.id)
continue
}

let displayCredential = DisplayCredentials(tld: tld, autofillDomainNameUrlMatcher: autofillDomainNameUrlMatcher, credential: credential)
credentials.append(displayCredential)
}

if !accountsFailedToLoad.isEmpty {
os_log("Failed to load credentials for accounts: %@", accountsFailedToLoad)
showErrorAlertFor(accountsFailedToLoad)
}

return credentials
} catch {
os_log("Failed to fetch accounts")
return []
}
}

private func showErrorAlertFor(_ accountIds: [String?]) {
let alert = NSAlert()
alert.messageText = "Failed to load credentials for accounts:"
alert.informativeText = accountIds.compactMap { $0 }.joined(separator: ", ")
alert.alertStyle = .warning
alert.addButton(withTitle: UserText.ok)
alert.runModal()
}
}
6 changes: 5 additions & 1 deletion DuckDuckGo/DeviceAuthentication/DeviceAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ final class DeviceAuthenticator: UserAuthenticating {
case exportLogins
case syncSettings
case deleteAllPasswords
case viewAllCredentials

var localizedDescription: String {
switch self {
Expand All @@ -51,6 +52,7 @@ final class DeviceAuthenticator: UserAuthenticating {
case .exportLogins: return UserText.pmAutoLockPromptExportLogins
case .syncSettings: return UserText.syncAutoLockPrompt
case .deleteAllPasswords: return UserText.deleteAllPasswordsPermissionText
case .viewAllCredentials: return UserText.pmAutoLockPromptUnlockLogins
}
}
}
Expand Down Expand Up @@ -163,7 +165,9 @@ final class DeviceAuthenticator: UserAuthenticating {
let needsAuthenticationForCreditCardsAutofill = reason == .autofillCreditCards && isCreditCardTimeIntervalExpired()
let needsAuthenticationForSyncSettings = reason == .syncSettings && isSyncSettingsTimeIntervalExpired()
let needsAuthenticationForDeleteAllPasswords = reason == .deleteAllPasswords
guard needsAuthenticationForCreditCardsAutofill || needsAuthenticationForSyncSettings || needsAuthenticationForDeleteAllPasswords || requiresAuthentication else {
let needsAuthenticationForViewAllCredentials = reason == .viewAllCredentials
guard needsAuthenticationForCreditCardsAutofill || needsAuthenticationForSyncSettings || needsAuthenticationForDeleteAllPasswords || needsAuthenticationForViewAllCredentials ||
requiresAuthentication else {
result(.success)
return
}
Expand Down
6 changes: 6 additions & 0 deletions DuckDuckGo/Menus/MainMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,12 @@ final class MainMenu: NSMenu {
.submenu(NetworkProtectionDebugMenu())
}

if #available(macOS 13.5, *) {
NSMenuItem(title: "Autofill") {
NSMenuItem(title: "View all Credentials", action: #selector(MainViewController.showAllCredentials)).withAccessibilityIdentifier("MainMenu.showAllCredentials")
}
}

NSMenuItem(title: "Simulate crash") {
NSMenuItem(title: "fatalError", action: #selector(MainViewController.triggerFatalError))
NSMenuItem(title: "NSException", action: #selector(MainViewController.crashOnException))
Expand Down
19 changes: 19 additions & 0 deletions DuckDuckGo/Menus/MainMenuActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import PixelKit
import Subscription
import WebKit
import os.log
import SwiftUI

// Actions are sent to objects of responder chain

Expand Down Expand Up @@ -964,6 +965,24 @@ extension MainViewController {
setConfigurationUrl(nil)
}

@available(macOS 13.5, *)
@objc func showAllCredentials(_ sender: Any?) {
let hostingView = NSHostingView(rootView: AutofillCredentialsDebugView())
hostingView.translatesAutoresizingMaskIntoConstraints = false
hostingView.frame.size = hostingView.intrinsicContentSize

let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 1400, height: 700),
styleMask: [.titled, .closable, .resizable],
backing: .buffered, defer: false)

window.center()
window.title = "Credentials"
window.contentView = hostingView
window.isReleasedWhenClosed = false
window.makeKeyAndOrderFront(nil)
}

// MARK: - Developer Tools

@objc func toggleDeveloperTools(_ sender: Any?) {
Expand Down
Loading