Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Fix #7810: Private Mode Biometric Authentication #7811

Merged
merged 4 commits into from
Aug 9, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ protocol TabTrayDelegate: AnyObject {
func tabOrderChanged()
}

class TabTrayController: LoadingViewController {
class TabTrayController: AuthenticationController {

typealias DataSource = UICollectionViewDiffableDataSource<TabTraySection, Tab>
typealias Snapshot = NSDiffableDataSourceSnapshot<TabTraySection, Tab>
Expand Down Expand Up @@ -47,7 +47,6 @@ class TabTrayController: LoadingViewController {
let tabManager: TabManager
let braveCore: BraveCoreMain

private let windowProtection: WindowProtection?
private var openTabsSessionServiceListener: OpenTabsSessionStateListener?
private var syncServicStateListener: AnyObject?

Expand Down Expand Up @@ -99,6 +98,7 @@ class TabTrayController: LoadingViewController {
var tabTrayMode: TabTrayMode = .local
private var privateModeCancellable: AnyCancellable?
private var initialScrollCompleted = false
private var localAuthObservers = Set<AnyCancellable>()

// MARK: User Interface Elements

Expand Down Expand Up @@ -188,9 +188,8 @@ class TabTrayController: LoadingViewController {
init(tabManager: TabManager, braveCore: BraveCoreMain, windowProtection: WindowProtection?) {
self.tabManager = tabManager
self.braveCore = braveCore
self.windowProtection = windowProtection

super.init(nibName: nil, bundle: nil)
super.init(windowProtection: windowProtection, isCancellable: true, unlockScreentitle: "Private Browsing is Locked")

if !UIAccessibility.isReduceMotionEnabled {
transitioningDelegate = self
Expand Down Expand Up @@ -304,6 +303,19 @@ class TabTrayController: LoadingViewController {
.sink(receiveValue: { [weak self] isPrivateBrowsing in
self?.updateColors(isPrivateBrowsing)
})

windowProtection?.cancelPressed
.sink { [weak self] _ in
self?.navigationController?.popViewController(animated: true)
}.store(in: &localAuthObservers)

windowProtection?.finalizedAuthentication
.sink { [weak self] success in
if success {
self?.toggleModeChanger()
}
self?.navigationController?.popViewController(animated: true)
}.store(in: &localAuthObservers)

reloadOpenTabsSession()

Expand Down Expand Up @@ -587,6 +599,14 @@ class TabTrayController: LoadingViewController {
}

@objc func togglePrivateModeAction() {
if !privateMode, Preferences.Privacy.privateBrowsingLock.value {
askForAuthentication(viewType: .tabTray)
} else {
toggleModeChanger()
}
}

func toggleModeChanger() {
tabTraySearchController.isActive = false

// Mode Change action disabled while drap-drop is active
Expand Down Expand Up @@ -624,7 +644,6 @@ class TabTrayController: LoadingViewController {

navigationController?.setNavigationBarHidden(privateMode, animated: false)
tabTypeSelector.isHidden = privateMode

}

func remove(tab: Tab) {
Expand Down
1 change: 1 addition & 0 deletions Sources/Brave/Frontend/ClientPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ extension Preferences {

final public class Privacy {
static let lockWithPasscode = Option<Bool>(key: "privacy.lock-with-passcode", default: false)
static let privateBrowsingLock = Option<Bool>(key: "privacy.private-browsing-lock", default: false)
/// Forces all private tabs
public static let privateBrowsingOnly = Option<Bool>(key: "privacy.private-only", default: false)
/// Blocks all cookies and access to local storage
Expand Down
31 changes: 29 additions & 2 deletions Sources/Brave/Frontend/Passcode/WindowProtection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@ public class WindowProtection {
private class LockedViewController: UIViewController {
let backgroundView = UIVisualEffectView(effect: UIBlurEffect(style: .systemThickMaterial))
let lockImageView = UIImageView(image: UIImage(named: "browser-lock-icon", in: .module, compatibleWith: nil)!)
let titleLabel = UILabel().then {
$0.font = .preferredFont(for: .title3, weight: .semibold)
$0.adjustsFontForContentSizeCategory = true
$0.textColor = .bravePrimary
$0.numberOfLines = 0
$0.textAlignment = .center
}
let unlockButton = FilledActionButton(type: .system).then {
$0.setTitle(Strings.unlockButtonTitle, for: .normal)
$0.titleLabel?.font = .preferredFont(forTextStyle: .headline)
$0.titleLabel?.adjustsFontForContentSizeCategory = true
$0.backgroundColor = .braveBlurpleTint
$0.isHidden = true
}

let cancelButton = ActionButton(type: .system).then {
$0.setTitle(Strings.cancelButtonTitle, for: .normal)
$0.titleLabel?.font = .preferredFont(forTextStyle: .headline)
Expand All @@ -39,12 +45,19 @@ public class WindowProtection {
super.viewDidLoad()

view.addSubview(backgroundView)
view.addSubview(titleLabel)
view.addSubview(lockImageView)
view.addSubview(unlockButton)
view.addSubview(cancelButton)
backgroundView.snp.makeConstraints {
$0.edges.equalTo(view)
}
titleLabel.snp.makeConstraints {
$0.leading.greaterThanOrEqualToSuperview().offset(20)
$0.trailing.lessThanOrEqualToSuperview().offset(-20)
$0.centerX.equalToSuperview()
$0.bottom.equalTo(lockImageView.snp.top).offset(-40)
}
lockImageView.snp.makeConstraints {
$0.center.equalTo(view)
}
Expand Down Expand Up @@ -103,12 +116,24 @@ public class WindowProtection {
}
}

private let onCancelPressed = PassthroughSubject<Void, Never>()
var unlockScreentitle: String = "" {
didSet {
lockedViewController.titleLabel.isHidden = unlockScreentitle.isEmpty
lockedViewController.titleLabel.text = unlockScreentitle
}
}

private let onCancelPressed = PassthroughSubject<Void, Never>()
private let didFinalizeAuthentication = PassthroughSubject<Bool, Never>()

var cancelPressed: AnyPublisher<Void, Never> {
onCancelPressed.eraseToAnyPublisher()
}

var finalizedAuthentication: AnyPublisher<Bool, Never> {
didFinalizeAuthentication.eraseToAnyPublisher()
}

public init?(window: UIWindow) {
guard let scene = window.windowScene else { return nil }
protectedWindow = window
Expand Down Expand Up @@ -208,6 +233,8 @@ public class WindowProtection {
Logger.module.error("Failed to unlock browser using local authentication: \(error.localizedDescription)")
}
}

self.didFinalizeAuthentication.send(success)
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion Sources/Brave/Frontend/Settings/SettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,16 @@ class SettingsViewController: TableViewController {
return Section(
header: .title(Strings.security),
rows: [
.boolRow(title: Strings.browserLock, detailText: Strings.browserLockDescription, option: Preferences.Privacy.lockWithPasscode, image: UIImage(braveSystemNamed: "leo.biometric.login")),
.boolRow(
title: Strings.Privacy.browserLock,
detailText: Strings.Privacy.browserLockDescription,
option: Preferences.Privacy.lockWithPasscode,
image: UIImage(braveSystemNamed: "leo.biometric.login")),
.boolRow(
title: Strings.Privacy.privateBrowsingLock,
detailText: Strings.Privacy.privateBrowsingLockDescription,
option: Preferences.Privacy.privateBrowsingLock,
image: UIImage(braveSystemNamed: "leo.lock")),
Row(
text: Strings.Login.loginListNavigationTitle,
selection: { [unowned self] in
Expand Down
46 changes: 2 additions & 44 deletions Sources/Brave/Frontend/Sync/SyncViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import Data
import LocalAuthentication
import Combine

class SyncViewController: UIViewController {
class SyncViewController: AuthenticationController {

let windowProtection: WindowProtection?
private let requiresAuthentication: Bool
private let isModallyPresented: Bool
private var localAuthObservers = Set<AnyCancellable>()

Expand All @@ -20,11 +18,8 @@ class SyncViewController: UIViewController {
requiresAuthentication: Bool = false,
isAuthenticationCancellable: Bool = true,
isModallyPresented: Bool = false) {
self.windowProtection = windowProtection
self.requiresAuthentication = requiresAuthentication
self.isModallyPresented = isModallyPresented

super.init(nibName: nil, bundle: nil)
super.init(windowProtection: windowProtection, requiresAuthentication: requiresAuthentication)

windowProtection?.isCancellable = isAuthenticationCancellable

Expand Down Expand Up @@ -67,48 +62,11 @@ class SyncViewController: UIViewController {
code()
}

/// A method to ask biometric authentication to user
/// - Parameter completion: block returning authentication status
func askForAuthentication(completion: ((Bool, LAError.Code?) -> Void)? = nil) {
guard let windowProtection = windowProtection else {
completion?(false, nil)
return
}

if !windowProtection.isPassCodeAvailable {
showSetPasscodeError() {
completion?(false, LAError.passcodeNotSet)
}
} else {
windowProtection.presentAuthenticationForViewController(
determineLockWithPasscode: false) { status, error in
completion?(status, error)
}
}
}

private func dismissSyncController() {
if isModallyPresented {
self.dismiss(animated: true)
} else {
self.navigationController?.popViewController(animated: true)
}
}

/// An alert presenter for passcode error to warn user to setup passcode to use feature
/// - Parameter completion: block after Ok button is pressed
private func showSetPasscodeError(completion: @escaping (() -> Void)) {
let alert = UIAlertController(
title: Strings.Sync.syncSetPasscodeAlertTitle,
message: Strings.Sync.syncSetPasscodeAlertDescription,
preferredStyle: .alert)

alert.addAction(
UIAlertAction(title: Strings.OKString, style: .default, handler: { _ in
completion()
})
)

present(alert, animated: true, completion: nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class SyncWelcomeViewController: SyncViewController {

private var overlayView: UIView?

private var isLoading: Bool = false {
override var isLoading: Bool {
didSet {
overlayView?.removeFromSuperview()

Expand Down
100 changes: 100 additions & 0 deletions Sources/Brave/Frontend/Widgets/LoadingViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2023 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

import UIKit
import BraveShared
import Shared
import LocalAuthentication

public class LoadingViewController: UIViewController {

let spinner = UIActivityIndicatorView().then {
$0.snp.makeConstraints { make in
make.size.equalTo(24)
}
$0.hidesWhenStopped = true
$0.isHidden = true
}

var isLoading: Bool = false {
didSet {
if isLoading {
view.addSubview(spinner)
spinner.snp.makeConstraints {
$0.center.equalTo(view.snp.center)
}
spinner.startAnimating()
} else {
spinner.stopAnimating()
spinner.removeFromSuperview()
}
}
}
}

public class AuthenticationController: LoadingViewController {
enum AuthViewType {
case sync, tabTray
}

let windowProtection: WindowProtection?
let requiresAuthentication: Bool

// MARK: Lifecycle

init(windowProtection: WindowProtection? = nil,
requiresAuthentication: Bool = false,
isCancellable: Bool = false,
unlockScreentitle: String = "") {
self.windowProtection = windowProtection
self.requiresAuthentication = requiresAuthentication

super.init(nibName: nil, bundle: nil)

self.windowProtection?.isCancellable = isCancellable
self.windowProtection?.unlockScreentitle = unlockScreentitle
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

/// A method to ask biometric authentication to user
/// - Parameter completion: block returning authentication status
func askForAuthentication(viewType: AuthViewType = .sync, completion: ((Bool, LAError.Code?) -> Void)? = nil) {
guard let windowProtection = windowProtection else {
completion?(false, nil)
return
}

if !windowProtection.isPassCodeAvailable {
showSetPasscodeError(viewType: viewType) {
completion?(false, LAError.passcodeNotSet)
}
} else {
windowProtection.presentAuthenticationForViewController(
determineLockWithPasscode: false) { status, error in
completion?(status, error)
}
}
}

/// An alert presenter for passcode error to warn user to setup passcode to use feature
/// - Parameter completion: block after Ok button is pressed
func showSetPasscodeError(viewType: AuthViewType, completion: @escaping (() -> Void)) {
let alert = UIAlertController(
title: Strings.Sync.syncSetPasscodeAlertTitle,
message: viewType == .sync ? Strings.Sync.syncSetPasscodeAlertDescription : Strings.Privacy.tabTraySetPasscodeAlertDescription,
preferredStyle: .alert)

alert.addAction(
UIAlertAction(title: Strings.OKString, style: .default, handler: { _ in
completion()
})
)

present(alert, animated: true, completion: nil)
}
}
26 changes: 0 additions & 26 deletions Sources/Brave/Frontend/Widgets/SiteTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,29 +130,3 @@ public class SiteTableViewController: LoadingViewController, UITableViewDelegate
true
}
}

public class LoadingViewController: UIViewController {

let spinner = UIActivityIndicatorView().then {
$0.snp.makeConstraints { make in
make.size.equalTo(24)
}
$0.hidesWhenStopped = true
$0.isHidden = true
}

var isLoading: Bool = false {
didSet {
if isLoading {
view.addSubview(spinner)
spinner.snp.makeConstraints {
$0.center.equalTo(view.snp.center)
}
spinner.startAnimating()
} else {
spinner.stopAnimating()
spinner.removeFromSuperview()
}
}
}
}
Loading