terminal/src/types/utils.cpp
Mike Griese c79334ffbb
Add a file for storing elevated-only state (#11222)
## Summary of the Pull Request

This creates an `elevated-state.json` that lives in `%LOCALAPPDATA%` next to `state.json`, that's only writable when elevated. It doesn't _use_ this file for anything, it just puts the framework down for use later.

It's _just like `ApplicationState`_. We'll use it the same way. 

It's readable when unelevated, which is nice, but not writable. If you're dumb and try to write to the file when unelevated, it'll just silently do nothing.

If we try opening the file and find out the permissions are different, we'll _blow the file away entirely_. This is to prevent someone from renaming the original file (which they can do unelevated), then slapping a new file that's writable by them down in it's place. 

## References
* We're going to use this in #11096, but these PRs need to be broken up.

## PR Checklist
* [x] Closes nothing
* [x] I work here
* [x] Tests added/passed
* [ ] Requires documentation to be updated - maybe? not sure we have docs on `state.json` at all yet

## Validation Steps Performed
I've played with this much more in `dev/migrie/f/non-terminal-content-elevation-warning`

###### followed by #11308, #11310
2021-11-13 01:58:43 +01:00

594 lines
20 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "inc/utils.hpp"
#include "inc/colorTable.hpp"
#include <wil/token_helpers.h>
using namespace Microsoft::Console;
// Routine Description:
// - Determines if a character is a valid number character, 0-9.
// Arguments:
// - wch - Character to check.
// Return Value:
// - True if it is. False if it isn't.
static constexpr bool _isNumber(const wchar_t wch) noexcept
{
return wch >= L'0' && wch <= L'9'; // 0x30 - 0x39
}
// Function Description:
// - Creates a String representation of a guid, in the format
// "{12345678-ABCD-EF12-3456-7890ABCDEF12}"
// Arguments:
// - guid: the GUID to create the string for
// Return Value:
// - a string representation of the GUID. On failure, throws E_INVALIDARG.
std::wstring Utils::GuidToString(const GUID guid)
{
return wil::str_printf<std::wstring>(L"{%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x}", guid.Data1, guid.Data2, guid.Data3, guid.Data4[0], guid.Data4[1], guid.Data4[2], guid.Data4[3], guid.Data4[4], guid.Data4[5], guid.Data4[6], guid.Data4[7]);
}
// Method Description:
// - Parses a GUID from a string representation of the GUID. Throws an exception
// if it fails to parse the GUID. See documentation of IIDFromString for
// details.
// Arguments:
// - wstr: a string representation of the GUID to parse
// Return Value:
// - A GUID if the string could successfully be parsed. On failure, throws the
// failing HRESULT.
GUID Utils::GuidFromString(_Null_terminated_ const wchar_t* str)
{
GUID result;
THROW_IF_FAILED(IIDFromString(str, &result));
return result;
}
// Method Description:
// - Creates a GUID, but not via an out parameter.
// Return Value:
// - A GUID if there's enough randomness; otherwise, an exception.
GUID Utils::CreateGuid()
{
GUID result{};
THROW_IF_FAILED(::CoCreateGuid(&result));
return result;
}
// Function Description:
// - Creates a String representation of a color, in the format "#RRGGBB"
// Arguments:
// - color: the COLORREF to create the string for
// Return Value:
// - a string representation of the color
std::string Utils::ColorToHexString(const til::color color)
{
std::stringstream ss;
ss << "#" << std::uppercase << std::setfill('0') << std::hex;
// Force the compiler to promote from byte to int. Without it, the
// stringstream will try to write the components as chars
ss << std::setw(2) << static_cast<int>(color.r);
ss << std::setw(2) << static_cast<int>(color.g);
ss << std::setw(2) << static_cast<int>(color.b);
return ss.str();
}
// Function Description:
// - Parses a color from a string. The string should be in the format "#RRGGBB" or "#RGB"
// Arguments:
// - str: a string representation of the COLORREF to parse
// Return Value:
// - A COLORREF if the string could successfully be parsed. If the string is not
// the correct format, throws E_INVALIDARG
til::color Utils::ColorFromHexString(const std::string_view str)
{
THROW_HR_IF(E_INVALIDARG, str.size() != 7 && str.size() != 4);
THROW_HR_IF(E_INVALIDARG, str.at(0) != '#');
std::string rStr;
std::string gStr;
std::string bStr;
if (str.size() == 4)
{
rStr = std::string(2, str.at(1));
gStr = std::string(2, str.at(2));
bStr = std::string(2, str.at(3));
}
else
{
rStr = std::string(&str.at(1), 2);
gStr = std::string(&str.at(3), 2);
bStr = std::string(&str.at(5), 2);
}
const BYTE r = gsl::narrow_cast<BYTE>(std::stoul(rStr, nullptr, 16));
const BYTE g = gsl::narrow_cast<BYTE>(std::stoul(gStr, nullptr, 16));
const BYTE b = gsl::narrow_cast<BYTE>(std::stoul(bStr, nullptr, 16));
return til::color{ r, g, b };
}
// Routine Description:
// - Given a color string, attempts to parse the color.
// The color are specified by name or RGB specification as per XParseColor.
// Arguments:
// - string - The string containing the color spec string to parse.
// Return Value:
// - An optional color which contains value if a color was successfully parsed
std::optional<til::color> Utils::ColorFromXTermColor(const std::wstring_view string) noexcept
{
auto color = ColorFromXParseColorSpec(string);
if (!color.has_value())
{
// Try again, but use the app color name parser
color = ColorFromXOrgAppColorName(string);
}
return color;
}
// Routine Description:
// - Given a color spec string, attempts to parse the color that's encoded.
//
// Based on the XParseColor documentation, the supported specs currently are the following:
// spec1: a color in the following format:
// "rgb:<red>/<green>/<blue>"
// spec2: a color in the following format:
// "#<red><green><blue>"
//
// In both specs, <color> is a value contains up to 4 hex digits, upper or lower case.
// Arguments:
// - string - The string containing the color spec string to parse.
// Return Value:
// - An optional color which contains value if a color was successfully parsed
std::optional<til::color> Utils::ColorFromXParseColorSpec(const std::wstring_view string) noexcept
try
{
bool foundXParseColorSpec = false;
bool foundValidColorSpec = false;
bool isSharpSignFormat = false;
size_t rgbHexDigitCount = 0;
std::array<unsigned int, 3> colorValues = { 0 };
std::array<unsigned int, 3> parameterValues = { 0 };
const auto stringSize = string.size();
// First we look for "rgb:"
// Other colorspaces are theoretically possible, but we don't support them.
auto curr = string.cbegin();
if (stringSize > 4)
{
auto prefix = std::wstring(string.substr(0, 4));
// The "rgb:" indicator should be case insensitive. To prevent possible issues under
// different locales, transform only ASCII range latin characters.
std::transform(prefix.begin(), prefix.end(), prefix.begin(), [](const auto x) {
return x >= L'A' && x <= L'Z' ? static_cast<wchar_t>(std::towlower(x)) : x;
});
if (prefix.compare(L"rgb:") == 0)
{
// If all the components have the same digit count, we can have one of the following formats:
// 9 "rgb:h/h/h"
// 12 "rgb:hh/hh/hh"
// 15 "rgb:hhh/hhh/hhh"
// 18 "rgb:hhhh/hhhh/hhhh"
// Note that the component sizes aren't required to be the same.
// Anything in between is also valid, e.g. "rgb:h/hh/h" and "rgb:h/hh/hhh".
// Any fewer cannot be valid, and any more will be too many. Return early in this case.
if (stringSize < 9 || stringSize > 18)
{
return std::nullopt;
}
foundXParseColorSpec = true;
std::advance(curr, 4);
}
}
// Try the sharp sign format.
if (!foundXParseColorSpec && stringSize > 1)
{
if (til::at(string, 0) == L'#')
{
// We can have one of the following formats:
// 4 "#hhh"
// 7 "#hhhhhh"
// 10 "#hhhhhhhhh"
// 13 "#hhhhhhhhhhhh"
// Any other cases will be invalid. Return early in this case.
if (!(stringSize == 4 || stringSize == 7 || stringSize == 10 || stringSize == 13))
{
return std::nullopt;
}
isSharpSignFormat = true;
foundXParseColorSpec = true;
rgbHexDigitCount = (stringSize - 1) / 3;
std::advance(curr, 1);
}
}
// No valid spec is found. Return early.
if (!foundXParseColorSpec)
{
return std::nullopt;
}
// Try to parse the actual color value of each component.
for (size_t component = 0; component < 3; component++)
{
bool foundColor = false;
auto& parameterValue = til::at(parameterValues, component);
// For "sharp sign" format, the rgbHexDigitCount is known.
// For "rgb:" format, colorspecs are up to hhhh/hhhh/hhhh, for 1-4 h's
const auto iteration = isSharpSignFormat ? rgbHexDigitCount : 4;
for (size_t i = 0; i < iteration && curr < string.cend(); i++)
{
const wchar_t wch = *curr++;
parameterValue *= 16;
unsigned int intVal = 0;
const auto ret = HexToUint(wch, intVal);
if (!ret)
{
// Encountered something weird oh no
return std::nullopt;
}
parameterValue += intVal;
if (isSharpSignFormat)
{
// If we get this far, any number can be seen as a valid part
// of this component.
foundColor = true;
if (i >= rgbHexDigitCount)
{
// Successfully parsed this component. Start the next one.
break;
}
}
else
{
// Record the hex digit count of the current component.
rgbHexDigitCount = i + 1;
// If this is the first 2 component...
if (component < 2 && curr < string.cend() && *curr == L'/')
{
// ...and we have successfully parsed this component, we need
// to skip the delimiter before starting the next one.
curr++;
foundColor = true;
break;
}
// Or we have reached the end of the string...
else if (curr >= string.cend())
{
// ...meaning that this is the last component. We're not going to
// see any delimiter. We can just break out.
foundColor = true;
break;
}
}
}
if (!foundColor)
{
// Indicates there was some error parsing color.
return std::nullopt;
}
// Calculate the actual color value based on the hex digit count.
auto& colorValue = til::at(colorValues, component);
const auto scaleMultiplier = isSharpSignFormat ? 0x10 : 0x11;
const auto scaleDivisor = scaleMultiplier << 8 >> 4 * (4 - rgbHexDigitCount);
colorValue = parameterValue * scaleMultiplier / scaleDivisor;
}
if (curr >= string.cend())
{
// We're at the end of the string and we have successfully parsed the color.
foundValidColorSpec = true;
}
// Only if we find a valid colorspec can we pass it out successfully.
if (foundValidColorSpec)
{
return til::color(LOBYTE(til::at(colorValues, 0)),
LOBYTE(til::at(colorValues, 1)),
LOBYTE(til::at(colorValues, 2)));
}
return std::nullopt;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return std::nullopt;
}
// Routine Description:
// - Converts a hex character to its equivalent integer value.
// Arguments:
// - wch - Character to convert.
// - value - receives the int value of the char
// Return Value:
// - true iff the character is a hex character.
bool Utils::HexToUint(const wchar_t wch,
unsigned int& value) noexcept
{
value = 0;
bool success = false;
if (wch >= L'0' && wch <= L'9')
{
value = wch - L'0';
success = true;
}
else if (wch >= L'A' && wch <= L'F')
{
value = (wch - L'A') + 10;
success = true;
}
else if (wch >= L'a' && wch <= L'f')
{
value = (wch - L'a') + 10;
success = true;
}
return success;
}
// Routine Description:
// - Converts a number string to its equivalent unsigned integer value.
// Arguments:
// - wstr - String to convert.
// - value - receives the int value of the string
// Return Value:
// - true iff the string is a unsigned integer string.
bool Utils::StringToUint(const std::wstring_view wstr,
unsigned int& value)
{
if (wstr.size() < 1)
{
return false;
}
unsigned int result = 0;
size_t current = 0;
while (current < wstr.size())
{
const wchar_t wch = wstr.at(current);
if (_isNumber(wch))
{
result *= 10;
result += wch - L'0';
++current;
}
else
{
return false;
}
}
value = result;
return true;
}
// Routine Description:
// - Split a string into different parts using the delimiter provided.
// Arguments:
// - wstr - String to split.
// - delimiter - delimiter to use.
// Return Value:
// - a vector containing the result string parts.
std::vector<std::wstring_view> Utils::SplitString(const std::wstring_view wstr,
const wchar_t delimiter) noexcept
try
{
std::vector<std::wstring_view> result;
size_t current = 0;
while (current < wstr.size())
{
const auto nextDelimiter = wstr.find(delimiter, current);
if (nextDelimiter == std::wstring::npos)
{
result.push_back(wstr.substr(current));
break;
}
else
{
const auto length = nextDelimiter - current;
result.push_back(wstr.substr(current, length));
// Skip this part and the delimiter. Start the next one
current += length + 1;
// The next index is larger than string size, which means the string
// is in the format of "part1;part2;" (assuming use ';' as delimiter).
// Add the last part which is an empty string.
if (current >= wstr.size())
{
result.push_back(L"");
}
}
}
return result;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return {};
}
// Routine Description:
// - Pre-process text pasted (presumably from the clipboard) with provided option.
// Arguments:
// - wstr - String to process.
// - option - option to use.
// Return Value:
// - The result string.
std::wstring Utils::FilterStringForPaste(const std::wstring_view wstr, const FilterOption option)
{
std::wstring filtered;
filtered.reserve(wstr.length());
const auto isControlCode = [](wchar_t c) {
if (c >= L'\x20' && c < L'\x7f')
{
// Printable ASCII characters.
return false;
}
if (c > L'\x9f')
{
// Not a control code.
return false;
}
// All C0 & C1 control codes will be removed except HT(0x09), LF(0x0a) and CR(0x0d).
return c != L'\x09' && c != L'\x0a' && c != L'\x0d';
};
std::wstring::size_type pos = 0;
std::wstring::size_type begin = 0;
while (pos < wstr.size())
{
const wchar_t c = til::at(wstr, pos);
if (WI_IsFlagSet(option, FilterOption::CarriageReturnNewline) && c == L'\n')
{
// copy up to but not including the \n
filtered.append(wstr.cbegin() + begin, wstr.cbegin() + pos);
if (!(pos > 0 && (til::at(wstr, pos - 1) == L'\r')))
{
// there was no \r before the \n we did not copy,
// so append our own \r (this effectively replaces the \n
// with a \r)
filtered.push_back(L'\r');
}
++pos;
begin = pos;
}
else if (WI_IsFlagSet(option, FilterOption::ControlCodes) && isControlCode(c))
{
// copy up to but not including the control code
filtered.append(wstr.cbegin() + begin, wstr.cbegin() + pos);
++pos;
begin = pos;
}
else
{
++pos;
}
}
// If we entered the while loop even once, begin would be non-zero
// (because we set begin = pos right after incrementing pos)
// So, if begin is still zero at this point it means we never found a newline
// and we can just write the original string
if (begin == 0)
{
return std::wstring{ wstr };
}
else
{
filtered.append(wstr.cbegin() + begin, wstr.cend());
// we may have removed some characters, so we may not need as much space
// as we reserved earlier
filtered.shrink_to_fit();
return filtered;
}
}
// Routine Description:
// - Shorthand check if a handle value is null or invalid.
// Arguments:
// - Handle
// Return Value:
// - True if non zero and not set to invalid magic value. False otherwise.
bool Utils::IsValidHandle(const HANDLE handle) noexcept
{
return handle != nullptr && handle != INVALID_HANDLE_VALUE;
}
// Function Description:
// - Generate a Version 5 UUID (specified in RFC4122 4.3)
// v5 UUIDs are stable given the same namespace and "name".
// Arguments:
// - namespaceGuid: The GUID of the v5 UUID namespace, which provides both
// a seed and a tacit agreement that all UUIDs generated
// with it will follow the same data format.
// - name: Bytes comprising the name (in a namespace-specific format)
// Return Value:
// - a new stable v5 UUID
GUID Utils::CreateV5Uuid(const GUID& namespaceGuid, const gsl::span<const gsl::byte> name)
{
// v5 uuid generation happens over values in network byte order, so let's enforce that
auto correctEndianNamespaceGuid{ EndianSwap(namespaceGuid) };
wil::unique_bcrypt_hash hash;
THROW_IF_NTSTATUS_FAILED(BCryptCreateHash(BCRYPT_SHA1_ALG_HANDLE, &hash, nullptr, 0, nullptr, 0, 0));
// According to N4713 8.2.1.11 [basic.lval], accessing the bytes underlying an object
// through unsigned char or char pointer *is defined*.
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hash.get(), reinterpret_cast<PUCHAR>(&correctEndianNamespaceGuid), sizeof(GUID), 0));
// BCryptHashData is ill-specified in that it leaves off "const" qualification for pbInput
THROW_IF_NTSTATUS_FAILED(BCryptHashData(hash.get(), reinterpret_cast<PUCHAR>(const_cast<gsl::byte*>(name.data())), gsl::narrow<ULONG>(name.size()), 0));
std::array<uint8_t, 20> buffer;
THROW_IF_NTSTATUS_FAILED(BCryptFinishHash(hash.get(), buffer.data(), gsl::narrow<ULONG>(buffer.size()), 0));
buffer.at(6) = (buffer.at(6) & 0x0F) | 0x50; // set the uuid version to 5
buffer.at(8) = (buffer.at(8) & 0x3F) | 0x80; // set the variant to 2 (RFC4122)
// We're using memcpy here pursuant to N4713 6.7.2/3 [basic.types],
// "...the underlying bytes making up the object can be copied into an array
// of char or unsigned char...array is copied back into the object..."
// std::copy may compile down to ::memcpy for these types, but using it might
// contravene the standard and nobody's got time for that.
GUID newGuid{ 0 };
::memcpy_s(&newGuid, sizeof(GUID), buffer.data(), sizeof(GUID));
return EndianSwap(newGuid);
}
bool Utils::IsElevated()
{
static bool isElevated = []() {
try
{
wil::unique_handle processToken{ GetCurrentProcessToken() };
const auto elevationType = wil::get_token_information<TOKEN_ELEVATION_TYPE>(processToken.get());
const auto elevationState = wil::get_token_information<TOKEN_ELEVATION>(processToken.get());
if (elevationType == TokenElevationTypeDefault && elevationState.TokenIsElevated)
{
// In this case, the user has UAC entirely disabled. This is sort of
// weird, we treat this like the user isn't an admin at all. There's no
// separation of powers, so the things we normally want to gate on
// "having special powers" doesn't apply.
//
// See GH#7754, GH#11096
return false;
}
return wil::test_token_membership(nullptr, SECURITY_NT_AUTHORITY, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return false;
}
}();
return isElevated;
}