// Copyright 2010-2021, Google Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
//     * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
//     * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

#include "base/run_level.h"

#ifdef OS_WIN
#include <aclapi.h>
#include <windows.h>
#endif  // OS_WIN

#ifdef __APPLE__
#include <unistd.h>
#endif  // __APPLE__

#if defined(OS_LINUX) || defined(OS_ANDROID)
#include <sys/types.h>
#include <unistd.h>
#endif  // OS_LINUX || OS_ANDROID

#include "base/const.h"
#include "base/logging.h"
#include "base/scoped_handle.h"
#include "base/system_util.h"
#include "base/util.h"
#include "base/win_sandbox.h"
#include "base/win_util.h"

namespace mozc {
namespace {

#ifdef OS_WIN
const wchar_t kElevatedProcessDisabledName[] = L"elevated_process_disabled";

// Returns true if both array have the same content.
template <typename T, size_t ArraySize>
bool AreEqualArray(const T (&lhs)[ArraySize], const T (&rhs)[ArraySize]) {
  for (size_t i = 0; i < ArraySize; ++i) {
    if (lhs[i] != rhs[i]) {
      return false;
    }
  }
  return true;
}

// returns true if the token was created by Secondary Logon
// (typically via RunAs command) or UAC (w/ alternative credential provided)
//  or if failed to determine.
bool IsDifferentUser(const HANDLE hToken) {
  TOKEN_SOURCE src;
  DWORD dwReturnedBc;

  // Get TOKEN_SOURCE
  if (!::GetTokenInformation(hToken, TokenSource, &src, sizeof(src),
                             &dwReturnedBc)) {
    // Most likely there was an error.
    return true;
  }

  // SourceName is not always null-terminated.
  //  We're looking for the cases marked '->'.
  //  Vista SP1 (Normal)                     "User32 \0"
  //  ->  Vista SP1 (RunAs):                 "seclogo\0"
  //  ->  Vista SP1 (Over-the-shoulder UAC): "CredPro\0"

  // Sacrifice the last character. That is practically ok for our purpose.
  src.SourceName[TOKEN_SOURCE_LENGTH - 1] = '\0';

  const char kSeclogo[] = "seclogo";
  const char kCredPro[] = "CredPro";

  return (AreEqualArray(kSeclogo, src.SourceName) ||
          AreEqualArray(kCredPro, src.SourceName));
}

// Returns true if UAC gave the high integrity level to the token
// or if failed to determine.
// This code is written by thatanaka
bool IsElevatedByUAC(const HANDLE hToken) {
  // Get TokenElevationType.
  DWORD dwSize;
  TOKEN_ELEVATION_TYPE ElevationType;
  if (!::GetTokenInformation(hToken, TokenElevationType, &ElevationType,
                             sizeof(ElevationType), &dwSize)) {
    return true;
  }

  // Only TokenElevationTypeFull means the process token was elevated by UAC.
  if (TokenElevationTypeFull != ElevationType) {
    return false;
  }

  // Although rare, it is still possible for an elevated token to have a lower
  // integrity level.
  // Checking to see if it is actually higher than medium.

  // Get TokenIntegrityLevel.
  BYTE buffer[sizeof(TOKEN_MANDATORY_LABEL) + SECURITY_MAX_SID_SIZE];
  if (!::GetTokenInformation(hToken, TokenIntegrityLevel, buffer,
                             sizeof(buffer), &dwSize)) {
    return true;
  }

  PTOKEN_MANDATORY_LABEL pMandatoryLabel =
      reinterpret_cast<PTOKEN_MANDATORY_LABEL>(buffer);

  // Since the SID was acquired from token, it should be always valid.
  DCHECK(::IsValidSid(pMandatoryLabel->Label.Sid));

  // Find the integrity level RID.
  PDWORD pdwIntegrityLevelRID;
  pdwIntegrityLevelRID = ::GetSidSubAuthority(pMandatoryLabel->Label.Sid,
                                              0 /* index 0 always exists */);
  if (!pdwIntegrityLevelRID) {
    return true;
  }

  return (SECURITY_MANDATORY_MEDIUM_RID < *pdwIntegrityLevelRID);
}
#endif  // OS_WIN
}  // namespace

RunLevel::RunLevelType RunLevel::GetRunLevel(RunLevel::RequestType type) {
#ifdef OS_WIN
  bool is_service_process = false;
  if (!WinUtil::IsServiceProcess(&is_service_process)) {
    // Returns DENY conservatively.
    return RunLevel::DENY;
  }
  if (is_service_process) {
    return RunLevel::DENY;
  }

  // Get process token
  HANDLE hProcessToken = nullptr;
  if (!::OpenProcessToken(::GetCurrentProcess(),
                          TOKEN_QUERY | TOKEN_QUERY_SOURCE, &hProcessToken)) {
    return RunLevel::DENY;
  }

  ScopedHandle process_token(hProcessToken);

  // Opt out of elevated process.
  if (CLIENT == type && GetElevatedProcessDisabled() &&
      mozc::IsElevatedByUAC(process_token.get())) {
    return RunLevel::DENY;
  }

  // Get thread token (if any)
  HANDLE hThreadToken = nullptr;
  if (!::OpenThreadToken(::GetCurrentThread(), TOKEN_QUERY, TRUE,
                         &hThreadToken) &&
      ERROR_NO_TOKEN != ::GetLastError()) {
    return RunLevel::DENY;
  }

  ScopedHandle thread_token(hThreadToken);

  // Thread token (if any) must not a service account.
  if (nullptr != thread_token.get()) {
    bool is_service_thread = false;
    if (!WinUtil::IsServiceUser(thread_token.get(), &is_service_thread)) {
      // Returns DENY conservatively.
      return RunLevel::DENY;
    }
    if (is_service_thread) {
      return RunLevel::DENY;
    }
  }

  // Check whether the server/renderer is running inside sandbox.
  if (type == SERVER || type == RENDERER) {
    // Restricted token must be created by sandbox.
    // Server is launched with NON_ADMIN so that it can use SSL access.
    // This is why it doesn't have restricted token. b/5502343
    if (type != SERVER && !::IsTokenRestricted(process_token.get())) {
      return RunLevel::DENY;
    }

    // Thread token must be created by sandbox.
    if (nullptr == thread_token.get()) {
      return RunLevel::DENY;
    }

    // Get the server path before the process is sandboxed.
    // SHGetFolderPath may fail in a sandboxed process.
    // See http://b/2301066 for details.
    const volatile std::string sys_dir = SystemUtil::GetServerDirectory();

    // Get the user profile path here because of the same reason.
    // See http://b/2301066 for details.
    const std::string user_dir = SystemUtil::GetUserProfileDirectory();

    std::wstring dir;
    Util::UTF8ToWide(user_dir, &dir);
    ScopedHandle dir_handle(::CreateFile(dir.c_str(), READ_CONTROL | WRITE_DAC,
                                         0, nullptr, OPEN_EXISTING,
                                         FILE_FLAG_BACKUP_SEMANTICS, 0));
    if (nullptr != dir_handle.get()) {
      BYTE buffer[sizeof(TOKEN_USER) + SECURITY_MAX_SID_SIZE];
      DWORD size = 0;
      if (::GetTokenInformation(thread_token.get(), TokenUser, buffer,
                                sizeof(buffer), &size)) {
        PTOKEN_USER ptoken_user = reinterpret_cast<PTOKEN_USER>(buffer);
        // Looks like in some environment, the profile foler's permission
        // includes Administrators but does not include the user himself.
        // In such a case, Mozc wouldn't work because Administrators identity
        // is removed by sandboxing so we should recover the permission here.
        // See http://b/2317718 for details.
        WinSandbox::AddKnownSidToKernelObject(
            dir_handle.get(), static_cast<SID *>(ptoken_user->User.Sid),
            SUB_CONTAINERS_AND_OBJECTS_INHERIT, GENERIC_ALL);
      }
    }

    // Revert from the impersonation token supplied by sandbox.
    // Note: This succeeds even when the thread is not impersonating.
    if (!::RevertToSelf()) {
      return RunLevel::DENY;
    }
  }

  // All DENY checks are passed.

  // Check whether the server/renderer is running as RunAs.
  if (type == SERVER || type == RENDERER) {
    // It's ok to do this check after RevertToSelf, as it's a process token
    // and also its handle was opened before.
    if (IsDifferentUser(process_token.get())) {
      // Run in RESTRICTED level in order to prevent the process from running
      // too long in another user's desktop.
      return RunLevel::RESTRICTED;
    }
  }

  return RunLevel::NORMAL;

#elif defined(OS_WASM)
  // WASM doesn't have runlevels. Always return normal.
  return RunLevel::NORMAL;
#else  // OS_WIN
  if (type == SERVER || type == RENDERER) {
    if (::geteuid() == 0) {
      // This process is started by root, or the executable is setuid to root.

      // TODO(yusukes): It would be better to add 'SAFE' run-level which
      //  prohibits all mutable operations to local resources and return the
      //  level after calling chroot("/somewhere/safe"), setgid("nogroup"),
      //  and setuid("nobody") here. This is because many novice Linux users
      //  tend to login to their desktop as root.

      return RunLevel::DENY;
    }
    if (::getuid() == 0) {
      // The executable is setuided to non-root and is started by root user?
      // This is unexpected. Returns DENY.
      return RunLevel::DENY;
    }
    return RunLevel::NORMAL;
  }

  // type is 'CLIENT'
  if (::geteuid() == 0 || ::getuid() == 0) {
    // When mozc.so is loaded into a privileged process, deny clients to use
    // dictionary_tool and config_dialog.
    return RunLevel::DENY;
  }

  return RunLevel::NORMAL;

#endif  // OS_WIN
}

bool RunLevel::IsProcessInJob() {
#ifdef OS_WIN
  // Check to see if we're in a job where
  // we can't create a child in our sandbox

  JOBOBJECT_EXTENDED_LIMIT_INFORMATION JobExtLimitInfo;
  // Get the job information of the current process
  if (!::QueryInformationJobObject(nullptr, JobObjectExtendedLimitInformation,
                                   &JobExtLimitInfo, sizeof(JobExtLimitInfo),
                                   nullptr)) {
    return false;
  }

  // Check to see if we can break away from the current job
  if (JobExtLimitInfo.BasicLimitInformation.LimitFlags &
      (JOB_OBJECT_LIMIT_BREAKAWAY_OK | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)) {
    // We're in a job, but it allows to break away.
    return false;
  }

  return true;
#else   // OS_WIN
  return false;
#endif  // OS_WIN
}

bool RunLevel::IsElevatedByUAC() {
#ifdef OS_WIN
  // Get process token
  HANDLE hProcessToken = nullptr;
  if (!::OpenProcessToken(::GetCurrentProcess(),
                          TOKEN_QUERY | TOKEN_QUERY_SOURCE, &hProcessToken)) {
    return false;
  }

  ScopedHandle process_token(hProcessToken);
  return mozc::IsElevatedByUAC(process_token.get());
#else   // OS_WIN
  return false;
#endif  // OS_WIN
}

bool RunLevel::SetElevatedProcessDisabled(bool disable) {
#ifdef OS_WIN
  HKEY key = 0;
  LONG result =
      ::RegCreateKeyExW(HKEY_CURRENT_USER, kElevatedProcessDisabledKey, 0,
                        nullptr, 0, KEY_WRITE, nullptr, &key, nullptr);

  if (ERROR_SUCCESS != result) {
    return false;
  }

  const DWORD value = disable ? 1 : 0;
  result =
      ::RegSetValueExW(key, kElevatedProcessDisabledName, 0, REG_DWORD,
                       reinterpret_cast<const BYTE *>(&value), sizeof(value));
  ::RegCloseKey(key);

  return ERROR_SUCCESS == result;
#else   // OS_WIN
  return false;
#endif  // OS_WIN
}

bool RunLevel::GetElevatedProcessDisabled() {
#ifdef OS_WIN
  HKEY key = 0;
  LONG result = ::RegOpenKeyExW(HKEY_CURRENT_USER, kElevatedProcessDisabledKey,
                                0, KEY_READ, &key);
  if (ERROR_SUCCESS != result) {
    return false;
  }

  DWORD value = 0;
  DWORD value_size = sizeof(value);
  DWORD value_type = 0;
  result =
      ::RegQueryValueEx(key, kElevatedProcessDisabledName, nullptr, &value_type,
                        reinterpret_cast<BYTE *>(&value), &value_size);
  ::RegCloseKey(key);

  if (ERROR_SUCCESS != result || value_type != REG_DWORD ||
      value_size != sizeof(value)) {
    return false;
  }

  return value > 0;
#else   // OS_WIN
  return false;
#endif  // OS_WIN
}
}  // namespace mozc
