diff --git a/browser/components/shell/Windows11LimitedAccessFeatures.cpp b/browser/components/shell/Windows11LimitedAccessFeatures.cpp index 1b436cb78fd2..7bbcb9c86502 100644 --- a/browser/components/shell/Windows11LimitedAccessFeatures.cpp +++ b/browser/components/shell/Windows11LimitedAccessFeatures.cpp @@ -19,10 +19,19 @@ static mozilla::LazyLogModule sLog("Windows11LimitedAccessFeatures"); // Fall back function defined in the #else #ifndef __MINGW32__ +# include "mozilla/ErrorResult.h" # include "nsString.h" +# include "nsCOMPtr.h" +# include "nsComponentManagerUtils.h" +# include "nsIWindowsRegKey.h" # include "nsWindowsHelpers.h" +# include "nsICryptoHash.h" # include "mozilla/Atomics.h" +# include "mozilla/Base64.h" +# include "mozilla/Char16.h" +# include "mozilla/WinHeaderOnlyUtils.h" +# include "WinUtils.h" # include # include @@ -110,24 +119,87 @@ using namespace mozilla; https://github.com/microsoft/Windows-classic-samples/tree/main/Samples/TaskbarManager/CppUnpackagedDesktopTaskbarPin */ -struct LimitedAccessFeatureInfo { - const nsCString debugName; - const nsString feature; - const nsString token; - const nsString attestation; -}; +/** + * Unlocks a Windows Limited Access Feature (LAF) by generating a token and + * attestation. + * + * This function first retrieves the LAF key from the registry using + * the LAF identifier and then combines the lafId, lafKey, and PFN + * into a token. + * + * Applying Base64(SHA256Encode("!!")[0..16]) yields the + * complete LAF token for unlocking. + * + * Taking the last 13 characters of the PFN yields the publisher identifier + * which is used in the following boilerplate: + * " has registered their use of with Microsoft and + * agrees to the terms of use." + * + * @return {LimitedAccessFeatureInfo} containing the generated + * token and attestation upon success. Contains empty strings for + * these fields upon failure. + */ +static mozilla::Result +GenerateLimitedAccessFeatureInfo(const nsCString& debugName, + const nsString& lafId) { + nsresult rv; + // Read registry key for a given Limited Access Feature with ID lafId. + nsAutoString keyData; + nsCOMPtr regKey = + do_CreateInstance("@mozilla.org/windows-registry-key;1", &rv); + NS_ENSURE_SUCCESS(rv, Err(rv)); + const nsAutoString regPath = + u"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModel\\LimitedAccessFeatures\\"_ns + + lafId; + rv = regKey->Open(nsIWindowsRegKey::ROOT_KEY_LOCAL_MACHINE, regPath, + nsIWindowsRegKey::ACCESS_READ); + NS_ENSURE_SUCCESS(rv, Err(rv)); + rv = regKey->ReadStringValue(u""_ns, keyData); + NS_ENSURE_SUCCESS(rv, Err(rv)); -static LimitedAccessFeatureInfo limitedAccessFeatureInfo[] = { - {// Win11LimitedAccessFeatureType::Taskbar - "Win11LimitedAccessFeatureType::Taskbar"_ns, - u"com.microsoft.windows.taskbar.pin"_ns, u"kRFiWpEK5uS6PMJZKmR7MQ=="_ns, - u"pcsmm0jrprpb2 has registered their use of "_ns - u"com.microsoft.windows.taskbar.pin with Microsoft and agrees to the "_ns - u"terms "_ns - u"of use."_ns}}; + // Get Package Family Name (PFN) and assemble a string to hash. + // First convert this string to SHA256, take the first 16 bytes + // only, and then read as base 64. + nsAutoCString hashStringResult; + nsAutoString encodedToken; + // The non-MSIX family name must match whatever value is in create_rc.py + // Currently this is MozillaFirefox_pcsmm0jrprpb2 + nsAutoString familyName; + if (widget::WinUtils::HasPackageIdentity()) { + familyName = nsDependentString(mozilla::GetPackageFamilyName().get()); + } else { + familyName = u"MozillaFirefox_pcsmm0jrprpb2"_ns; + } + const nsAutoCString hashString = + NS_ConvertUTF16toUTF8(lafId + u"!"_ns + keyData + u"!"_ns + familyName); -static_assert(mozilla::ArrayLength(limitedAccessFeatureInfo) == - kWin11LimitedAccessFeatureTypeCount); + nsCOMPtr cryptoHash = + do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, Err(rv)); + rv = cryptoHash->Init(nsICryptoHash::SHA256); + NS_ENSURE_SUCCESS(rv, Err(rv)); + rv = cryptoHash->Update(reinterpret_cast(hashString.get()), + hashString.Length()); + NS_ENSURE_SUCCESS(rv, Err(rv)); + rv = cryptoHash->Finish(false, hashStringResult); + NS_ENSURE_SUCCESS(rv, Err(rv)); + + // Keep only first 16 bytes and encode + hashStringResult.Truncate(hashStringResult.Length() - 16); + rv = Base64Encode(hashStringResult, encodedToken); + NS_ENSURE_SUCCESS(rv, Err(rv)); + + // The PFN contains a package ID in the last 13 characters. + // This ID is based on the value in the publisher field of the + // AppManifest. This ID is used to assemble the attestation. + familyName.Cut(0, familyName.Length() - 13); + nsAutoString attestation = + familyName + u" has registered their use of "_ns + lafId + + u" with Microsoft and agrees to the terms of use."_ns; + LimitedAccessFeatureInfo result = {debugName, lafId, encodedToken, + attestation}; + return result; +} /** Implementation of the Win11LimitedAccessFeaturesInterface. @@ -141,7 +213,7 @@ class Win11LimitedAccessFeatures : public Win11LimitedAccessFeaturesInterface { private: AtomicState& GetState(Win11LimitedAccessFeatureType feature); Result UnlockImplementation( - Win11LimitedAccessFeatureType feature); + const LimitedAccessFeatureInfo& lafInfo); /** * Store the state as an atomic so that it can be safely accessed from @@ -175,6 +247,18 @@ Result Win11LimitedAccessFeatures::Unlock( Win11LimitedAccessFeatureType feature) { AtomicState& atomicState = GetState(feature); + // Win11LimitedAccessFeatureType::Taskbar + auto taskbarLafInfo = GenerateLimitedAccessFeatureInfo( + "Win11LimitedAccessFeatureType::Taskbar"_ns, + u"com.microsoft.windows.taskbar.pin"_ns); + if (taskbarLafInfo.isErr()) { + LAF_LOG(LogLevel::Debug, "Unlocking taskbar failed with error %d", + NS_ERROR_GET_CODE(taskbarLafInfo.unwrapErr())); + return Err(E_FAIL); + } + + LimitedAccessFeatureInfo limitedAccessFeatureInfo[] = { + taskbarLafInfo.unwrap()}; const auto& lafInfo = limitedAccessFeatureInfo[static_cast(feature)]; LAF_LOG(LogLevel::Debug, @@ -193,7 +277,7 @@ Result Win11LimitedAccessFeatures::Unlock( // both threads will unlock the feature. This situation is unlikely, but even // if it happens, it's not a problem. - auto result = UnlockImplementation(feature); + auto result = UnlockImplementation(lafInfo); int newState = Locked; if (!result.isErr() && result.unwrap()) { @@ -223,12 +307,10 @@ Win11LimitedAccessFeatures::AtomicState& Win11LimitedAccessFeatures::GetState( } Result Win11LimitedAccessFeatures::UnlockImplementation( - Win11LimitedAccessFeatureType feature) { + const LimitedAccessFeatureInfo& lafInfo) { ComPtr limitedAccessFeatures; ComPtr limitedAccessFeaturesResult; - const auto& lafInfo = limitedAccessFeatureInfo[static_cast(feature)]; - HRESULT hr = RoGetActivationFactory( HStringReference( RuntimeClass_Windows_ApplicationModel_LimitedAccessFeatures) @@ -279,4 +361,10 @@ CreateWin11LimitedAccessFeaturesInterface() { return result; } +static mozilla::Result +GenerateLimitedAccessFeatureInfo(const nsCString& debugName, + const nsString& lafId) { + return Err(NS_ERROR_NOT_IMPLEMENTED); +} + #endif diff --git a/browser/components/shell/Windows11LimitedAccessFeatures.h b/browser/components/shell/Windows11LimitedAccessFeatures.h index 8e1ae5db7a5b..2b621a623854 100644 --- a/browser/components/shell/Windows11LimitedAccessFeatures.h +++ b/browser/components/shell/Windows11LimitedAccessFeatures.h @@ -7,6 +7,7 @@ #define SHELL_WINDOWS11LIMITEDACCESSFEATURES_H__ #include "nsISupportsImpl.h" +#include "nsString.h" #include "mozilla/Result.h" #include "mozilla/ResultVariant.h" #include @@ -47,7 +48,18 @@ class Win11LimitedAccessFeaturesInterface { virtual ~Win11LimitedAccessFeaturesInterface() {} }; +struct LimitedAccessFeatureInfo { + const nsCString debugName; + const nsString feature; + const nsString token; + const nsString attestation; +}; + RefPtr CreateWin11LimitedAccessFeaturesInterface(); +mozilla::Result +GenerateLimitedAccessFeatureInfo(const nsCString& debugName, + const nsString& lafId); + #endif // SHELL_WINDOWS11LIMITEDACCESSFEATURES_H__ diff --git a/browser/components/shell/test/gtest/LimitedAccessFeatureTests.cpp b/browser/components/shell/test/gtest/LimitedAccessFeatureTests.cpp new file mode 100644 index 000000000000..ddf3a75481c5 --- /dev/null +++ b/browser/components/shell/test/gtest/LimitedAccessFeatureTests.cpp @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. */ + +#include "gtest/gtest.h" + +#include "Windows11LimitedAccessFeatures.h" +#include "WinUtils.h" + +TEST(LimitedAccessFeature, VerifyGeneratedInfo) +{ + // If running on MSIX we have no guarantee that the + // generated LAF info will match the known values. + if (mozilla::widget::WinUtils::HasPackageIdentity()) { + return; + } + + LimitedAccessFeatureInfo knownLafInfo = { + // Win11LimitedAccessFeatureType::Taskbar + "Win11LimitedAccessFeatureType::Taskbar"_ns, // debugName + u"com.microsoft.windows.taskbar.pin"_ns, // feature + u"kRFiWpEK5uS6PMJZKmR7MQ=="_ns, // token + u"pcsmm0jrprpb2 has registered their use of "_ns // attestation + u"com.microsoft.windows.taskbar.pin with Microsoft and agrees to the "_ns + u"terms "_ns + u"of use."_ns}; + + auto generatedLafInfoResult = GenerateLimitedAccessFeatureInfo( + "Win11LimitedAccessFeatureType::Taskbar"_ns, + u"com.microsoft.windows.taskbar.pin"_ns); + ASSERT_TRUE(generatedLafInfoResult.isOk()); + LimitedAccessFeatureInfo generatedLafInfo = generatedLafInfoResult.unwrap(); + + // Check for equality between generated values and known good values + ASSERT_TRUE(knownLafInfo.debugName.Equals(generatedLafInfo.debugName)); + ASSERT_TRUE(knownLafInfo.feature.Equals(generatedLafInfo.feature)); + ASSERT_TRUE(knownLafInfo.token.Equals(generatedLafInfo.token)); + ASSERT_TRUE(knownLafInfo.attestation.Equals(generatedLafInfo.attestation)); +} diff --git a/browser/components/shell/test/gtest/moz.build b/browser/components/shell/test/gtest/moz.build index 89e378e19fdb..e33786fad66f 100644 --- a/browser/components/shell/test/gtest/moz.build +++ b/browser/components/shell/test/gtest/moz.build @@ -8,6 +8,7 @@ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": LOCAL_INCLUDES += ["/browser/components/shell"] UNIFIED_SOURCES += [ + "LimitedAccessFeatureTests.cpp", "ShellLinkTests.cpp", ] diff --git a/widget/windows/docs/LimitedAccessFeature.rst b/widget/windows/docs/LimitedAccessFeature.rst new file mode 100644 index 000000000000..89d4a3eaafd1 --- /dev/null +++ b/widget/windows/docs/LimitedAccessFeature.rst @@ -0,0 +1,58 @@ +=============================== +Windows Limited Access Features +=============================== + +-------- +Overview +-------- + +`Limited Access Features (LAF) +`_ are +features which require a special token and attestation to unlock them before +their corresponding APIs can be called. These usually take the form +``com.microsoft.windows.featureFamily.name``. This is most relevant to Firefox in +the context of pinning to the Windows taskbar as the new Windows pinning APIs require +Firefox to first unlock the corresponding ``com.microsoft.windows.taskbar.pin`` +LAF. + +If we need to use a new Limited Access Feature we should notify Microsoft +if requested in the feature's documentation. + +------------------- +Unlocking Procedure +------------------- + +Applications which exist in a packaged context, such as MSIX installs, +have something called a Package Family Name (PFN). The PFN is generated +at build time for MSIX installs and varies between channels. This can be +accessed through Windows API calls on MSIX. For non-MSIX installs we are +provided a specific PFN by Microsoft which lives in the rc file in +the final install and can be modified in ``create_rc.py``. + +The registry key corresponding to the LAF can be read at +``HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModel\LimitedAccessFeatures\`` +This key's default value contains a string which is the "key" for the +Limited Access Feature. + +To get the complete unlocking token, the LAF identifier, LAF key, and PFN +can be combined in the format ``"!!"`` and then +encoded in SHA256. Taking the first 16 characters of this output and +converting to Base64 yields the final token. The overall process is +as follows: + +.. code:: text + + Base64(SHA256Encode("!!")[0..16]) + +The other piece of unlocking is the attestation. We first need +the publisher identifier, which consists of the last 13 characters +of the PFN. With that, the attestation is assembled using the boilerplate +below: + +.. code:: text + + has registered their use of + with Microsoft and agrees to the terms of use. + +The token and attestation can then be passed into ``LimitedAccessFeature.TryUnlockFeature()`` +to unlock the corresponding APIs for use. diff --git a/widget/windows/docs/index.rst b/widget/windows/docs/index.rst index 9a24cb9cdbce..b3d16afe0467 100644 --- a/widget/windows/docs/index.rst +++ b/widget/windows/docs/index.rst @@ -7,3 +7,4 @@ Firefox on Windows blocklist windows-pointing-device/index + LimitedAccessFeature