chromium/chrome/credential_provider/gaiacp/password_recovery_manager.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/credential_provider/gaiacp/password_recovery_manager.h"

#include <windows.h>

#include <lm.h>  // Needed for LSA_UNICODE_STRING
#include <process.h>
#include <winternl.h>

#include <string_view>

#define _NTDEF_  // Prevent redefition errors, must come after <winternl.h>
#include <ntsecapi.h>  // For POLICY_ALL_ACCESS types

#include <algorithm>

#include "base/base64.h"
#include "base/containers/span.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/strings/strcat.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/credential_provider/common/gcp_strings.h"
#include "chrome/credential_provider/gaiacp/gcp_utils.h"
#include "chrome/credential_provider/gaiacp/logging.h"
#include "chrome/credential_provider/gaiacp/mdm_utils.h"
#include "chrome/credential_provider/gaiacp/reg_utils.h"
#include "chrome/credential_provider/gaiacp/scoped_lsa_policy.h"
#include "chrome/credential_provider/gaiacp/win_http_url_fetcher.h"
#include "crypto/aead.h"
#include "third_party/boringssl/src/include/openssl/aead.h"
#include "third_party/boringssl/src/include/openssl/bytestring.h"
#include "third_party/boringssl/src/include/openssl/err.h"
#include "third_party/boringssl/src/include/openssl/evp.h"
#include "third_party/boringssl/src/include/openssl/rand.h"
#include "third_party/boringssl/src/include/openssl/rsa.h"
#include "third_party/boringssl/src/include/openssl/x509.h"

namespace credential_provider {

const base::TimeDelta
    PasswordRecoveryManager::kDefaultEscrowServiceEncryptionKeyRequestTimeout =
        base::Milliseconds(12000);

const base::TimeDelta
    PasswordRecoveryManager::kDefaultEscrowServiceDecryptionKeyRequestTimeout =
        base::Milliseconds(3000);

namespace {

typedef std::vector<std::pair<std::string, std::string*>>
    UrlFetchResultNeedOutputs;

// Constants for storing password recovery information in the LSA.
constexpr char kUserPasswordLsaStoreIdKey[] = "resource_id";
constexpr char kUserPasswordLsaStoreEncryptedPasswordKey[] =
    "encrypted_password";

// Constants used for contacting the password escrow service.
const char kEscrowServiceGenerateKeyPairPath[] = "/v1/generatekeypair";
const char kGenerateKeyPairRequestDeviceIdParameterName[] = "deviceId";
const char kGenerateKeyPairResponsePublicKeyParameterName[] = "base64PublicKey";
const char kGenerateKeyPairResponseResourceIdParameterName[] = "resourceId";

const char kEscrowServiceGetPrivateKeyPath[] = "/v1/getprivatekey";
const char kGetPrivateKeyResponsePrivateKeyParameterName[] = "base64PrivateKey";

// Constants used during padding and unpadding given secret.
constexpr char kPaddedPassword[] = "password";

constexpr char kPasswordLength[] = "password_length";

constexpr char kPaddingChar = '-';

constexpr size_t kMinPaddedPasswordLength = 64;

// Constants used during encrypting and decrypting given secret.
constexpr size_t kNonceLength = 12;

constexpr size_t kSessionKeyLength = 32;

// Maximum number of retries if a HTTP call to the backend fails.
constexpr unsigned int kMaxNumHttpRetries = 3;

bool Base64DecodeCryptographicKey(const std::string& cryptographic_key,
                                  std::string* out) {
  std::string cryptographic_key_copy;
  base::RemoveChars(cryptographic_key, "\n", &cryptographic_key_copy);
  if (!base::Base64Decode(cryptographic_key_copy, out)) {
    LOGFN(ERROR) << "Base64Decode failed";
    return false;
  }
  return true;
}

// Callback to log password encryption/decryption errors.
static int LogBoringSSLError(const char* str, size_t len, void* ctx) {
  LOGFN(ERROR) << std::string_view(str, len);
  return 1;
}

// PadSecret pads the given |secret| with kPaddingChar and serializes the padded
// secret into JSON along with original secret length.
bool PadSecret(const std::string& secret, std::string* out) {
  size_t padded_length = (secret.size() + kMinPaddedPasswordLength - 1) &
                         ~(kMinPaddedPasswordLength - 1);
  std::string padded_secret(padded_length, kPaddingChar);
  std::memcpy(&padded_secret[padded_length - secret.size()], secret.data(),
              secret.size());

  auto pwd_padding_dict =
      base::Value::Dict()
          .Set(kPaddedPassword, padded_secret)
          .Set(kPasswordLength, static_cast<int>(secret.size()));
  SecurelyClearString(padded_secret);

  auto result = base::JSONWriter::Write(pwd_padding_dict, out);
  if (auto* password_value = pwd_padding_dict.FindString(kPaddedPassword)) {
    SecurelyClearString(*password_value);
  }
  return result;
}

// UnpadSecret deserializes given |padded_secret| into json object and tries to
// find padded secret. It then removes the padding and returns original secret.
bool UnpadSecret(const std::string& serialized_padded_secret,
                 std::string* out) {
  std::optional<base::Value::Dict> pwd_padding = base::JSONReader::ReadDict(
      serialized_padded_secret, base::JSON_ALLOW_TRAILING_COMMAS);
  if (!pwd_padding) {
    LOGFN(ERROR) << "Failed to deserialize given secret from json.";
    return false;
  }

  auto* padded_secret = pwd_padding->FindString(kPaddedPassword);
  auto pwd_length = pwd_padding->FindInt(kPasswordLength);

  auto result = true;
  if (!padded_secret || !pwd_length.has_value()) {
    result = false;
  } else {
    out->assign(&(*padded_secret)[padded_secret->size() - *pwd_length],
                *pwd_length);
  }
  SecurelyClearDictionaryValueWithKey(pwd_padding, kPaddedPassword);

  return result;
}

// Encrypts the given |secret| with the provided |public_key|. Returns a vector
// of uint8_t as the encrypted secret.
std::optional<std::vector<uint8_t>> PublicKeyEncrypt(
    const std::string& public_key,
    const std::string& secret) {
  CBS pub_key_cbs;
  CBS_init(&pub_key_cbs, reinterpret_cast<const uint8_t*>(&public_key[0]),
           public_key.size());
  bssl::UniquePtr<EVP_PKEY> pub_key(EVP_parse_public_key(&pub_key_cbs));
  if (!pub_key || CBS_len(&pub_key_cbs)) {
    ERR_print_errors_cb(&LogBoringSSLError, /*unused*/ nullptr);
    return std::nullopt;
  }

  RSA* rsa = EVP_PKEY_get0_RSA(pub_key.get());
  if (!rsa) {
    ERR_print_errors_cb(&LogBoringSSLError, /*unused*/ nullptr);
    return std::nullopt;
  }

  // Generate a random session key and random nonce.
  uint8_t session_key_with_nonce[kSessionKeyLength + kNonceLength];
  RAND_bytes(session_key_with_nonce, sizeof(session_key_with_nonce));

  // Encrypt the session key with the RSA public key.
  size_t rsa_len;
  std::vector<uint8_t> ciphertext(RSA_size(rsa));
  if (!RSA_encrypt(rsa, &rsa_len, ciphertext.data(), ciphertext.size(),
                   session_key_with_nonce, sizeof(session_key_with_nonce),
                   RSA_PKCS1_OAEP_PADDING)) {
    ERR_print_errors_cb(&LogBoringSSLError, /*unused*/ nullptr);
    return std::nullopt;
  }

  std::string session_key(session_key_with_nonce,
                          session_key_with_nonce + kSessionKeyLength);

  std::string sealed_secret;
  crypto::Aead aead(crypto::Aead::AES_256_GCM);
  aead.Init(&session_key);
  aead.Seal(secret,
            std::string_view(reinterpret_cast<const char*>(
                                 &session_key_with_nonce[kSessionKeyLength]),
                             kNonceLength),
            /*ad=*/"", &sealed_secret);

  ciphertext.insert(ciphertext.end(), sealed_secret.data(),
                    sealed_secret.data() + sealed_secret.size());
  return ciphertext;
}

// Decrypts the provided |ciphertext| with the given |private_key|. Returns
// an std::optional<std::string> as the decrypted secret.
std::optional<std::string> PrivateKeyDecrypt(
    const std::string& private_key,
    base::span<const uint8_t> ciphertext) {
  CBS priv_key_cbs;
  CBS_init(&priv_key_cbs, reinterpret_cast<const uint8_t*>(&private_key[0]),
           private_key.size());
  bssl::UniquePtr<EVP_PKEY> priv_key(EVP_parse_private_key(&priv_key_cbs));
  if (!priv_key || CBS_len(&priv_key_cbs)) {
    ERR_print_errors_cb(&LogBoringSSLError, /*unused*/ nullptr);
    return std::nullopt;
  }

  RSA* rsa = EVP_PKEY_get0_RSA(priv_key.get());
  if (!rsa) {
    LOGFN(ERROR) << "No RSA is found in EVP_PKEY_get0_RSA";
    return std::nullopt;
  }
  const size_t rsa_size = RSA_size(rsa);
  if (ciphertext.size() < rsa_size) {
    LOGFN(ERROR) << "Incorrect RSA size for given cipher text";
    return std::nullopt;
  }

  // Decrypt the encrypted session key using given provided key.
  std::vector<uint8_t> session_key_with_nonce(rsa_size);
  size_t session_key_with_nonce_len;
  if (!RSA_decrypt(rsa, &session_key_with_nonce_len,
                   session_key_with_nonce.data(), session_key_with_nonce.size(),
                   ciphertext.data(), rsa_size, RSA_PKCS1_OAEP_PADDING)) {
    ERR_print_errors_cb(&LogBoringSSLError, /*unused*/ nullptr);
    return std::nullopt;
  }
  session_key_with_nonce.resize(session_key_with_nonce_len);

  std::string session_key(session_key_with_nonce.data(),
                          session_key_with_nonce.data() + kSessionKeyLength);

  std::string plaintext;
  crypto::Aead aead(crypto::Aead::AES_256_GCM);
  aead.Init(&session_key);
  aead.Open(
      std::string_view(reinterpret_cast<const char*>(&ciphertext[rsa_size]),
                       ciphertext.size() - rsa_size),
      std::string_view(reinterpret_cast<const char*>(
                           &session_key_with_nonce[kSessionKeyLength]),
                       kNonceLength),
      /*ad=*/"", &plaintext);

  return plaintext;
}

// Request a new public key and corresponding resource id from the escrow
// service in order to encrypt |password|. |access_token| is used to authorize
// the request on the escrow service. |device_id| is used to identify the device
// making the request. Fills in |encrypted_data| the resource id for the
// encryption key and also with the encryped password.
HRESULT EncryptUserPasswordUsingEscrowService(
    const std::string& access_token,
    const std::string& device_id,
    const std::wstring& password,
    const base::TimeDelta& request_timeout,
    std::optional<base::Value::Dict>& encrypted_data) {
  DCHECK(!encrypted_data);

  std::string resource_id;
  std::string public_key;
  base::Value::Dict request_dict;
  request_dict.Set(kGenerateKeyPairRequestDeviceIdParameterName, device_id);
  std::optional<base::Value> request_result;

  // Fetch the results and extract the |resource_id| for the key and the
  // |public_key| to be used for encryption.
  HRESULT hr = WinHttpUrlFetcher::BuildRequestAndFetchResultFromHttpService(
      PasswordRecoveryManager::Get()->GetEscrowServiceGenerateKeyPairUrl(),
      access_token, {}, request_dict, request_timeout, kMaxNumHttpRetries,
      &request_result);

  if (FAILED(hr)) {
    LOGFN(ERROR) << "BuildRequestAndFetchResultFromHttpService hr="
                 << putHR(hr);
    return E_FAIL;
  }

  if (!request_result.has_value() || !request_result->is_dict() ||
      !ExtractKeysFromDict(
          request_result->GetDict(),
          {
              {kGenerateKeyPairResponseResourceIdParameterName, &resource_id},
              {kGenerateKeyPairResponsePublicKeyParameterName, &public_key},
          })) {
    return E_FAIL;
  }

  std::string decoded_public_key;
  if (!Base64DecodeCryptographicKey(public_key, &decoded_public_key)) {
    LOGFN(ERROR) << "Failed to base64 decode public key";
    return E_FAIL;
  }

  std::string password_utf8 = base::WideToUTF8(password);
  std::string padded_password;
  auto result = PadSecret(password_utf8, &padded_password);
  SecurelyClearString(password_utf8);
  if (!result) {
    LOGFN(ERROR) << "Failed while padding password";
    return E_FAIL;
  }

  auto opt = PublicKeyEncrypt(decoded_public_key, padded_password);
  SecurelyClearString(padded_password);
  if (opt == std::nullopt) {
    return E_FAIL;
  }

  std::string cipher_text = base::Base64Encode(*opt);

  encrypted_data =
      base::Value::Dict()
          .Set(kUserPasswordLsaStoreIdKey, resource_id)
          .Set(kUserPasswordLsaStoreEncryptedPasswordKey, cipher_text);

  return hr;
}

// Given the |encrypted_data_dict| which would contain the resource id of the
// encryption key and the encrypted password, recovers the |decrypted_password|
// by getting the private key from the escrow service and decrypting the
// password. |access_token| is used to authorize the request on the escrow
// service.
HRESULT DecryptUserPasswordUsingEscrowService(
    const std::string& access_token,
    const base::Value::Dict& encrypted_data_dict,
    const base::TimeDelta& request_timeout,
    std::wstring* decrypted_password) {
  DCHECK(decrypted_password);
  const std::string* resource_id =
      encrypted_data_dict.FindString(kUserPasswordLsaStoreIdKey);
  const std::string* encoded_cipher_text =
      encrypted_data_dict.FindString(kUserPasswordLsaStoreEncryptedPasswordKey);

  if (!resource_id) {
    LOGFN(ERROR) << "No password resource id found to restore";
    return E_FAIL;
  }

  if (!encoded_cipher_text) {
    LOGFN(ERROR) << "No encrypted password found to restore";
    return E_FAIL;
  }

  std::string private_key;
  std::optional<base::Value> request_result;

  // Fetch the results and extract the |private_key| to be used for decryption.
  HRESULT hr = WinHttpUrlFetcher::BuildRequestAndFetchResultFromHttpService(
      PasswordRecoveryManager::Get()->GetEscrowServiceGetPrivateKeyUrl(
          *resource_id),
      access_token, {}, {}, request_timeout, kMaxNumHttpRetries,
      &request_result);

  if (FAILED(hr)) {
    LOGFN(ERROR) << "BuildRequestAndFetchResultFromHttpService hr="
                 << putHR(hr);
    return E_FAIL;
  }

  if (!request_result.has_value() || !request_result->is_dict() ||
      !ExtractKeysFromDict(
          request_result->GetDict(),
          {
              {kGetPrivateKeyResponsePrivateKeyParameterName, &private_key},
          })) {
    return E_FAIL;
  }

  std::string decoded_cipher_text;
  if (!base::Base64Decode(*encoded_cipher_text, &decoded_cipher_text)) {
    LOGFN(ERROR) << "Failed to base64 decode ciphertext";
    return E_FAIL;
  }

  std::string decoded_private_key;
  if (!Base64DecodeCryptographicKey(private_key, &decoded_private_key)) {
    LOGFN(ERROR) << "Failed to base64 decode private key";
    return E_FAIL;
  }

  auto decrypted_secret =
      PrivateKeyDecrypt(decoded_private_key,
                        base::as_bytes(base::make_span(decoded_cipher_text)));

  if (decrypted_secret == std::nullopt) {
    return E_FAIL;
  }

  std::string unpadded;
  UnpadSecret(*decrypted_secret, &unpadded);
  *decrypted_password = base::UTF8ToWide(unpadded);

  SecurelyClearString(*decrypted_secret);
  SecurelyClearString(unpadded);

  return S_OK;
}

}  // namespace

// static
PasswordRecoveryManager* PasswordRecoveryManager::Get() {
  return *GetInstanceStorage();
}

// static
PasswordRecoveryManager** PasswordRecoveryManager::GetInstanceStorage() {
  static PasswordRecoveryManager instance(
      kDefaultEscrowServiceEncryptionKeyRequestTimeout,
      kDefaultEscrowServiceDecryptionKeyRequestTimeout);
  static PasswordRecoveryManager* instance_storage = &instance;

  return &instance_storage;
}

PasswordRecoveryManager::PasswordRecoveryManager(
    base::TimeDelta encryption_key_timeout,
    base::TimeDelta decryption_key_timeout)
    : encryption_key_request_timeout_(encryption_key_timeout),
      decryption_key_request_timeout_(decryption_key_timeout) {}

PasswordRecoveryManager::~PasswordRecoveryManager() = default;

HRESULT PasswordRecoveryManager::ClearUserRecoveryPassword(
    const std::wstring& sid) {
  auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);

  if (!policy) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "ScopedLsaPolicy::Create hr=" << putHR(hr);
    return hr;
  }
  std::wstring store_key = GetUserPasswordLsaStoreKey(sid);
  return policy->RemovePrivateData(store_key.c_str());
}

HRESULT PasswordRecoveryManager::StoreWindowsPasswordIfNeeded(
    const std::wstring& sid,
    const std::string& access_token,
    const std::wstring& password) {
  if (!PasswordRecoveryEnabled())
    return E_NOTIMPL;

  std::wstring machine_guid;
  HRESULT hr = GetMachineGuid(&machine_guid);

  if (FAILED(hr)) {
    LOGFN(ERROR) << "Failed to get machine GUID hr=" << putHR(hr);
    return hr;
  }

  std::string device_id = base::WideToUTF8(machine_guid);

  auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);

  if (!policy) {
    hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "ScopedLsaPolicy::Create hr=" << putHR(hr);
    return hr;
  }

  // See if a password key is already stored in the LSA for this user.
  std::wstring store_key = GetUserPasswordLsaStoreKey(sid);

  if (policy->PrivateDataExists(store_key.c_str())) {
    return S_OK;
  }

  std::optional<base::Value::Dict> encrypted_dict;
  hr = EncryptUserPasswordUsingEscrowService(access_token, device_id, password,
                                             encryption_key_request_timeout_,
                                             encrypted_dict);
  if (SUCCEEDED(hr)) {
    std::string lsa_value;
    if (base::JSONWriter::Write(*encrypted_dict, &lsa_value)) {
      std::wstring lsa_value16 = base::UTF8ToWide(lsa_value);
      hr = policy->StorePrivateData(store_key.c_str(), lsa_value16.c_str());
      SecurelyClearString(lsa_value16);
      SecurelyClearString(lsa_value);

      if (FAILED(hr)) {
        LOGFN(ERROR) << "StorePrivateData hr=" << putHR(hr);
        return hr;
      }

      LOGFN(VERBOSE) << "Encrypted and stored secret for sid=" << sid;
    } else {
      LOGFN(ERROR) << "base::JSONWriter::Write failed";
      return E_FAIL;
    }

    SecurelyClearDictionaryValueWithKey(
        encrypted_dict, kUserPasswordLsaStoreEncryptedPasswordKey);
  } else {
    LOGFN(ERROR) << "EncryptUserPasswordUsingEscrowService hr=" << putHR(hr);
    return E_FAIL;
  }
  return S_OK;
}

HRESULT PasswordRecoveryManager::RecoverWindowsPasswordIfPossible(
    const std::wstring& sid,
    const std::string& access_token,
    std::wstring* recovered_password) {
  if (!PasswordRecoveryEnabled())
    return E_NOTIMPL;

  DCHECK(recovered_password);

  auto policy = ScopedLsaPolicy::Create(POLICY_ALL_ACCESS);

  if (!policy) {
    HRESULT hr = HRESULT_FROM_WIN32(::GetLastError());
    LOGFN(ERROR) << "ScopedLsaPolicy::Create hr=" << putHR(hr);
    return hr;
  }

  // See if a password key is already stored in the LSA for this user.
  std::wstring store_key = GetUserPasswordLsaStoreKey(sid);
  wchar_t password_lsa_data[1024];
  HRESULT hr = policy->RetrievePrivateData(store_key.c_str(), password_lsa_data,
                                           std::size(password_lsa_data));

  if (FAILED(hr))
    LOGFN(ERROR) << "RetrievePrivateData hr=" << putHR(hr);

  std::string json_string = base::WideToUTF8(password_lsa_data);
  std::optional<base::Value::Dict> encrypted_dict =
      base::JSONReader::ReadDict(json_string, base::JSON_ALLOW_TRAILING_COMMAS);
  SecurelyClearString(json_string);
  SecurelyClearBuffer(password_lsa_data, sizeof(password_lsa_data));

  if (encrypted_dict) {
    std::wstring decrypted_password;
    hr = DecryptUserPasswordUsingEscrowService(access_token, *encrypted_dict,
                                               decryption_key_request_timeout_,
                                               &decrypted_password);

    SecurelyClearDictionaryValueWithKey(
        encrypted_dict, kUserPasswordLsaStoreEncryptedPasswordKey);

    if (SUCCEEDED(hr)) {
      *recovered_password = decrypted_password;
    }
    SecurelyClearString(decrypted_password);
  }

  LOGFN(VERBOSE) << "Decrypted the secret for sid=" << sid;

  return hr;
}

GURL PasswordRecoveryManager::GetEscrowServiceGenerateKeyPairUrl() {
  if (!PasswordRecoveryEnabled())
    return GURL();

  GURL escrow_service_server = EscrowServiceUrl();

  if (escrow_service_server.is_empty()) {
    LOGFN(ERROR) << "No escrow service server specified";
    return GURL();
  }

  return escrow_service_server.Resolve(kEscrowServiceGenerateKeyPairPath);
}

GURL PasswordRecoveryManager::GetEscrowServiceGetPrivateKeyUrl(
    const std::string& resource_id) {
  if (!PasswordRecoveryEnabled())
    return GURL();

  GURL escrow_service_server = EscrowServiceUrl();

  if (escrow_service_server.is_empty()) {
    LOGFN(ERROR) << "No escrow service server specified";
    return GURL();
  }

  return escrow_service_server.Resolve(
      base::StrCat({kEscrowServiceGetPrivateKeyPath, "/", resource_id}));
}

std::string PasswordRecoveryManager::MakeGenerateKeyPairResponseForTesting(
    const std::string& public_key,
    const std::string& resource_id) {
  return base::StringPrintf(
      R"({"%s": "%s", "%s": "%s"})",
      kGenerateKeyPairResponsePublicKeyParameterName, public_key.c_str(),
      kGenerateKeyPairResponseResourceIdParameterName, resource_id.c_str());
}

std::string PasswordRecoveryManager::MakeGetPrivateKeyResponseForTesting(
    const std::string& private_key) {
  return base::StringPrintf(R"({"%s": "%s"})",
                            kGetPrivateKeyResponsePrivateKeyParameterName,
                            private_key.c_str());
}

}  // namespace credential_provider