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

Commit

Permalink
Fix #7810: Private Mode Biometric Authentication (#7811)
Browse files Browse the repository at this point in the history
  • Loading branch information
soner-yuksel authored Aug 9, 2023
1 parent 8afb991 commit ff67224
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 81 deletions.
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
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)
/// Whether or not private browsing tabs can be session restored (persistent private browsing)
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

0 comments on commit ff67224

Please sign in to comment.