Chrome on macOS: purge stale screen capture permission

If Chrome 97 or earlier was used to screen share a stale designated
requirement may be cached in the system TCC.db. This stale record can
cause issues starting with Chrome 98.0.4758.132 (extended stable),
99.0.4844.74 (stable), 100.0.4896.45 (beta), 101.0.4929.5 (dev),
101.0.4933.0 (canary). These are the first releases to be signed with
the new Developer ID certificate (https://crbug.com/1263152).

This CL will attempt to purge stale or thought to be stale screen
capture records at early startup on macOS 10.15+. See
https://crbug.com/1307502#c11 for more details.

Without the TCC reset, the checkbox in System Preferences:Security &
Privacy:Privacy:Screen Recording is wrong—it will show Chrome as
approved (checked checkbox) based on its bundle ID, but contemporary
Chromes will not match the saved designated requirement. Users looking
at the checked checkbox will see that they’ve given Chrome access, but
the system will not actually allow it access. The TCC reset revokes
Chrome’s permission based on bundle ID, so the next attempt to access
the screen will be treated the same as the initial attempt in a fresh
installation. The system will create a new entry with the updated
designated requirement on first access, the user will see an unchecked
checkbox, and by checking it, will grant Chrome access, which the
system will respect.

This doesn’t carry existing screen recording permission granted to
archaic Chromes forward to modern Chromes, but it does make it so that
the established UI flow for inspecting and granting permission works as
intended and tracks reality.

(cherry picked from commit 682276951958656e68188b004f4109b90a9ecc15)

Bug: 1307502
Change-Id: I88cf37fefc6511a9406bb8ebcab2b3e25e938e04
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3535659
Reviewed-by: Mark Mentovai <mark@chromium.org>
Commit-Queue: Tom Burgin <bur@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#983016}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3540053
Commit-Queue: Mark Mentovai <mark@chromium.org>
Auto-Submit: Mark Mentovai <mark@chromium.org>
Commit-Queue: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Bot-Commit: Rubber Stamper <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/4951@{#24}
Cr-Branched-From: 27de6227ca357da0d57ae2c7b18da170c4651438-refs/heads/main@{#982481}
diff --git a/base/threading/thread_restrictions.h b/base/threading/thread_restrictions.h
index 79c2a4b..a22b320 100644
--- a/base/threading/thread_restrictions.h
+++ b/base/threading/thread_restrictions.h
@@ -150,6 +150,7 @@
 namespace chrome {
 #if BUILDFLAG(IS_MAC)
 void DeveloperIDCertificateReauthorizeInApp();
+void PurgeStaleScreenCapturePermission();
 #endif  // BUILDFLAG(IS_MAC)
 }  // namespace chrome
 namespace chromecast {
@@ -498,6 +499,7 @@
   friend bool PathProviderWin(int, FilePath*);
 #if BUILDFLAG(IS_MAC)
   friend void chrome::DeveloperIDCertificateReauthorizeInApp();
+  friend void chrome::PurgeStaleScreenCapturePermission();
 #endif  // BUILDFLAG(IS_MAC)
   friend bool chromeos::system::IsCoreSchedulingAvailable();
   friend int chromeos::system::NumberOfPhysicalCores();
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index fbceb854..1b42ed9 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -5664,6 +5664,8 @@
       "mac/mac_startup_profiler.h",
       "mac/nsprocessinfo_additions.h",
       "mac/nsprocessinfo_additions.mm",
+      "mac/purge_stale_screen_capture_permission.h",
+      "mac/purge_stale_screen_capture_permission.mm",
       "mac/relauncher.h",
       "mac/relauncher.mm",
       "media/webrtc/media_authorization_wrapper_mac.h",
diff --git a/chrome/browser/chrome_browser_main_mac.mm b/chrome/browser/chrome_browser_main_mac.mm
index 831d717..8bf722c 100644
--- a/chrome/browser/chrome_browser_main_mac.mm
+++ b/chrome/browser/chrome_browser_main_mac.mm
@@ -27,6 +27,7 @@
 #include "chrome/browser/mac/install_from_dmg.h"
 #import "chrome/browser/mac/keystone_glue.h"
 #include "chrome/browser/mac/mac_startup_profiler.h"
+#include "chrome/browser/mac/purge_stale_screen_capture_permission.h"
 #include "chrome/browser/ui/cocoa/main_menu_builder.h"
 #include "chrome/common/channel_info.h"
 #include "chrome/common/chrome_paths.h"
@@ -121,6 +122,7 @@
   [app_controller mainMenuCreated];
 
   chrome::DeveloperIDCertificateReauthorizeInApp();
+  chrome::PurgeStaleScreenCapturePermission();
 
   PrefService* local_state = g_browser_process->local_state();
   DCHECK(local_state);
diff --git a/chrome/browser/mac/purge_stale_screen_capture_permission.h b/chrome/browser/mac/purge_stale_screen_capture_permission.h
new file mode 100644
index 0000000..d273443
--- /dev/null
+++ b/chrome/browser/mac/purge_stale_screen_capture_permission.h
@@ -0,0 +1,14 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_MAC_PURGE_STALE_SCREEN_CAPTURE_PERMISSION_H_
+#define CHROME_BROWSER_MAC_PURGE_STALE_SCREEN_CAPTURE_PERMISSION_H_
+
+namespace chrome {
+
+void PurgeStaleScreenCapturePermission();
+
+}  // namespace chrome
+
+#endif  // CHROME_BROWSER_MAC_PURGE_STALE_SCREEN_CAPTURE_PERMISSION_H_
diff --git a/chrome/browser/mac/purge_stale_screen_capture_permission.mm b/chrome/browser/mac/purge_stale_screen_capture_permission.mm
new file mode 100644
index 0000000..e9a9c2d
--- /dev/null
+++ b/chrome/browser/mac/purge_stale_screen_capture_permission.mm
@@ -0,0 +1,159 @@
+// Copyright 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/mac/purge_stale_screen_capture_permission.h"
+
+#include <CoreFoundation/CoreFoundation.h>
+#import <Foundation/Foundation.h>
+#include <Security/Security.h>
+
+#include <string>
+
+#include "base/files/file_path.h"
+#include "base/logging.h"
+#include "base/mac/bundle_locations.h"
+#include "base/mac/foundation_util.h"
+#include "base/mac/mac_logging.h"
+#include "base/process/launch.h"
+#include "base/process/process.h"
+#include "base/strings/string_util.h"
+#include "base/strings/sys_string_conversions.h"
+#include "base/threading/thread_restrictions.h"
+#include "build/branding_buildflags.h"
+#include "sql/database.h"
+#include "sql/statement.h"
+
+namespace chrome {
+
+namespace {
+
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+
+bool RequirementForBundleIDNeedsReset(NSString* bundle_id) {
+  base::FilePath local_application_support_path;
+  if (!base::mac::GetLocalDirectory(NSApplicationSupportDirectory,
+                                    &local_application_support_path)) {
+    return true;
+  }
+  base::FilePath local_tcc_db_path(
+      local_application_support_path.Append("com.apple.TCC").Append("TCC.db"));
+  sql::DatabaseOptions options;
+  options.exclusive_locking = false;
+  sql::Database tcc_db = sql::Database(options);
+
+  if (!tcc_db.Open(local_tcc_db_path)) {
+    // On macOS 10.15 and macOS 11 this open is expected to fail due to SIP.
+    return true;
+  }
+  sql::Statement s(tcc_db.GetUniqueStatement(
+      "SELECT csreq FROM access WHERE client_type=0 AND client=? AND "
+      "service='kTCCServiceScreenCapture'"));
+  s.BindString(0, bundle_id.UTF8String);
+
+  while (s.Step()) {
+    base::span<const uint8_t> csreq_blob = s.ColumnBlob(0);
+    if (csreq_blob.empty()) {
+      return true;
+    }
+
+    base::ScopedCFTypeRef<CFDataRef> csreq_data(
+        CFDataCreate(nullptr, csreq_blob.data(), csreq_blob.size()));
+    base::ScopedCFTypeRef<SecRequirementRef> requirement;
+    OSStatus status = SecRequirementCreateWithData(
+        csreq_data, kSecCSDefaultFlags, requirement.InitializeInto());
+    if (status != errSecSuccess) {
+      OSSTATUS_LOG(ERROR, status) << "SecRequirementCreateWithData";
+      return true;
+    }
+    base::ScopedCFTypeRef<CFStringRef> requirement_string;
+    status = SecRequirementCopyString(requirement, kSecCSDefaultFlags,
+                                      requirement_string.InitializeInto());
+    if (status != errSecSuccess) {
+      OSSTATUS_LOG(ERROR, status) << "SecRequirementCopyString";
+      return true;
+    }
+
+    static constexpr char kCurrentRequirementTail[] =
+        " and certificate leaf[subject.OU] = EQHXZ8M8AV";
+    if (!base::EndsWith(base::SysCFStringRefToUTF8(requirement_string),
+                        kCurrentRequirementTail)) {
+      return true;
+    }
+  }
+
+  return !s.Succeeded();
+}
+
+bool ResetTCCScreenCaptureForBundleID(NSString* bundle_id) {
+  if (!bundle_id.length) {
+    return false;
+  }
+  std::vector<std::string> argv = {"/usr/bin/tccutil", "reset", "ScreenCapture",
+                                   bundle_id.UTF8String};
+
+  base::LaunchOptions launch_options;
+  base::Process p = base::LaunchProcess(argv, launch_options);
+  int status;
+  return p.WaitForExit(&status) && status == 0;
+}
+
+bool AttemptPurgeStaleScreenCapturePermission() {
+  NSString* bundle_identifier = base::mac::MainBundle().bundleIdentifier;
+  if (RequirementForBundleIDNeedsReset(bundle_identifier)) {
+    // Paranoia about the exec failing for some reason. Retry once for good
+    // measure.
+    for (int i = 0; i < 2; ++i) {
+      if (ResetTCCScreenCaptureForBundleID(bundle_identifier)) {
+        // The stale record has been purged.
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // The requirement is valid or doesn't exist.
+  return true;
+}
+
+#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
+
+}  // namespace
+
+void PurgeStaleScreenCapturePermission() {
+#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
+
+  // TCC doesn’t supervise screen capture until macOS 10.15.
+  if (@available(macOS 10.15, *)) {
+    static NSString* const kPreferenceKeyBase =
+        @"PurgeStaleScreenCapturePermissionSuccessEarly2022";
+    static NSString* const kPreferenceKeyAttemptsSuffix = @"Attempts";
+    static NSString* const kPreferenceKeySuccessSuffix = @"Success";
+    NSString* success_key = [kPreferenceKeyBase
+        stringByAppendingString:kPreferenceKeySuccessSuffix];
+    NSString* attempts_key = [kPreferenceKeyBase
+        stringByAppendingString:kPreferenceKeyAttemptsSuffix];
+
+    NSUserDefaults* user_defaults = [NSUserDefaults standardUserDefaults];
+    if ([user_defaults boolForKey:success_key]) {
+      return;
+    }
+
+    const int kAttemptsMax = 3;
+    int attempts_value = [user_defaults integerForKey:attempts_key];
+    if ((attempts_value >= kAttemptsMax)) {
+      return;
+    }
+    [user_defaults setInteger:++attempts_value forKey:attempts_key];
+    base::ScopedAllowBlocking allow_blocking;
+    if (AttemptPurgeStaleScreenCapturePermission()) {
+      // Future startups will now return from PurgeStaleScreenCapturePermission
+      // early.
+      [user_defaults setBool:YES forKey:success_key];
+    }
+  }
+
+#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
+}
+
+}  // namespace chrome