text: // Copyright (c) 2010 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.
  //
  // This class isn't pretty. It's just a step better than globals, which is what
  // these were previously.
  
  #include "chrome/browser/sync/util/user_settings.h"
  
  #include "build/build_config.h"
  
  #if defined(OS_WIN)
  #include <windows.h>
  #endif
  
  #include <limits>
  #include <string>
  #include <vector>
  
  #include "base/file_util.h"
  #include "base/string_util.h"
  #include "chrome/browser/sync/syncable/directory_manager.h"  // For migration.
  #include "chrome/browser/sync/util/crypto_helpers.h"
  #include "chrome/browser/sync/util/data_encryption.h"
  #include "chrome/common/sqlite_utils.h"
  
  using std::numeric_limits;
  using std::string;
  using std::vector;
  
  using syncable::DirectoryManager;
  
  namespace browser_sync {
  
  void ExecOrDie(sqlite3* dbhandle, const char *query) {
    SQLStatement statement;
    statement.prepare(dbhandle, query);
    if (SQLITE_DONE != statement.step()) {
      LOG(FATAL) << query << "\n" << sqlite3_errmsg(dbhandle);
    }
  }
  
  // Useful for encoding any sequence of bytes into a string that can be used in
  // a table name. Kind of like hex encoding, except that A is zero and P is 15.
  string APEncode(const string& in) {
    string result;
    result.reserve(in.size() * 2);
    for (string::const_iterator i = in.begin(); i != in.end(); ++i) {
      unsigned int c = static_cast<unsigned char>(*i);
      result.push_back((c & 0x0F) + 'A');
      result.push_back(((c >> 4) & 0x0F) + 'A');
    }
    return result;
  }
  
  string APDecode(const string& in) {
    string result;
    result.reserve(in.size() / 2);
    for (string::const_iterator i = in.begin(); i != in.end(); ++i) {
      unsigned int c = *i - 'A';
      if (++i != in.end())
        c = c | (static_cast<unsigned char>(*i - 'A') << 4);
      result.push_back(c);
    }
    return result;
  }
  
  static const char PASSWORD_HASH[] = "password_hash2";
  static const char SALT[] = "salt2";
  
  static const int kSaltSize = 20;
  static const int kCurrentDBVersion = 12;
  
  UserSettings::ScopedDBHandle::ScopedDBHandle(UserSettings* settings)
      : mutex_lock_(settings->dbhandle_mutex_), handle_(&settings->dbhandle_) {
  }
  
  UserSettings::UserSettings() : dbhandle_(NULL) {
  }
  
  string UserSettings::email() const {
    base::AutoLock lock(mutex_);
    return email_;
  }
  
  static void MakeSigninsTable(sqlite3* const dbhandle) {
    // Multiple email addresses can map to the same Google Account. This table
    // keeps a map of sign-in email addresses to primary Google Account email
    // addresses.
    ExecOrDie(dbhandle,
              "CREATE TABLE signins"
              " (signin, primary_email, "
              " PRIMARY KEY(signin, primary_email) ON CONFLICT REPLACE)");
  }
  
  void UserSettings::MigrateOldVersionsAsNeeded(sqlite3* const handle,
      int current_version) {
    switch (current_version) {
      // Versions 1-9 are unhandled.  Version numbers greater than
      // kCurrentDBVersion should have already been weeded out by the caller.
      default:
        // When the version is too old, we just try to continue anyway.  There
        // should not be a released product that makes a database too old for us
        // to handle.
        LOG(WARNING) << "UserSettings database version " << current_version <<
            " is too old to handle.";
        return;
      case 10:
        {
          // Scrape the 'shares' table to find the syncable DB.  'shares' had a
          // pair of string columns that mapped the username to the filename of
          // the sync data sqlite3 file.  Version 11 switched to a constant
          // filename, so here we read the string, copy the file to the new name,
          // delete the old one, and then drop the unused shares table.
          SQLStatement share_query;
          share_query.prepare(handle, "SELECT share_name, file_name FROM shares");
          int query_result = share_query.step();
          CHECK(SQLITE_ROW == query_result);
          FilePath::StringType share_name, file_name;
  #if defined(OS_POSIX)
          share_name = share_query.column_string(0);
          file_name = share_query.column_string(1);
  #else
          share_name = share_query.column_wstring(0);
          file_name = share_query.column_wstring(1);
  #endif
  
          const FilePath& src_syncdata_path = FilePath(file_name);
          FilePath dst_syncdata_path(src_syncdata_path.DirName());
          file_util::AbsolutePath(&dst_syncdata_path);
          dst_syncdata_path = dst_syncdata_path.Append(
              DirectoryManager::GetSyncDataDatabaseFilename());
          if (!file_util::Move(src_syncdata_path, dst_syncdata_path)) {
            LOG(WARNING) << "Unable to upgrade UserSettings from v10";
            return;
          }
        }
        ExecOrDie(handle, "DROP TABLE shares");
        ExecOrDie(handle, "UPDATE db_version SET version = 11");
      // FALL THROUGH
      case 11:
        ExecOrDie(handle, "DROP TABLE signin_types");
        ExecOrDie(handle, "UPDATE db_version SET version = 12");
      // FALL THROUGH
      case kCurrentDBVersion:
        // Nothing to migrate.
        return;
    }
  }
  
  static void MakeCookiesTable(sqlite3* const dbhandle) {
    // This table keeps a list of auth tokens for each signed in account. There
    // will be as many rows as there are auth tokens per sign in.
    // The service_token column will store encrypted values.
    ExecOrDie(dbhandle,
              "CREATE TABLE cookies"
              " (email, service_name, service_token, "
              " PRIMARY KEY(email, service_name) ON CONFLICT REPLACE)");
  }
  
  static void MakeClientIDTable(sqlite3* const dbhandle) {
    // Stores a single client ID value that can be used as the client id, if
    // there's not another such ID provided on the install.
    ExecOrDie(dbhandle, "CREATE TABLE client_id (id) ");
    {
      SQLStatement statement;
      statement.prepare(dbhandle,
                        "INSERT INTO client_id values ( ? )");
      statement.bind_string(0, Generate128BitRandomHexString());
      if (SQLITE_DONE != statement.step()) {
        LOG(FATAL) << "INSERT INTO client_id\n" << sqlite3_errmsg(dbhandle);
      }
    }
  }
  
  bool UserSettings::Init(const FilePath& settings_path) {
    {  // Scope the handle.
      ScopedDBHandle dbhandle(this);
      if (dbhandle_)
        sqlite3_close(dbhandle_);
  
      if (SQLITE_OK != sqlite_utils::OpenSqliteDb(settings_path, &dbhandle_))
        return false;
  
      // In the worst case scenario, the user may hibernate his computer during
      // one of our transactions.
      sqlite3_busy_timeout(dbhandle_, numeric_limits<int>::max());
      ExecOrDie(dbhandle.get(), "PRAGMA fullfsync = 1");
      ExecOrDie(dbhandle.get(), "PRAGMA synchronous = 2");
  
      SQLTransaction transaction(dbhandle.get());
      transaction.BeginExclusive();
      SQLStatement table_query;
      table_query.prepare(dbhandle.get(),
                          "select count(*) from sqlite_master"
                          " where type = 'table' and name = 'db_version'");
      int query_result = table_query.step();
      CHECK(SQLITE_ROW == query_result);
      int table_count = table_query.column_int(0);
      table_query.reset();
      if (table_count > 0) {
        SQLStatement version_query;
        version_query.prepare(dbhandle.get(),
                              "SELECT version FROM db_version");
        query_result = version_query.step();
        CHECK(SQLITE_ROW == query_result);
        const int version = version_query.column_int(0);
        version_query.reset();
        if (version > kCurrentDBVersion) {
          LOG(WARNING) << "UserSettings database is too new.";
          return false;
        }
  
        MigrateOldVersionsAsNeeded(dbhandle.get(), version);
      } else {
        // Create settings table.
        {
          SQLStatement statement;
          statement.prepare(dbhandle.get(),
                            "CREATE TABLE settings"
                            " (email, key, value, "
                            "  PRIMARY KEY(email, key) ON CONFLICT REPLACE)");
          if (SQLITE_DONE != statement.step()) {
            return false;
          }
        }
        // Create and populate version table.
        {
          SQLStatement statement;
          statement.prepare(dbhandle.get(),
                            "CREATE TABLE db_version ( version )");
          if (SQLITE_DONE != statement.step()) {
            return false;
          }
        }
        {
          SQLStatement statement;
          statement.prepare(dbhandle.get(),
                            "INSERT INTO db_version values ( ? )");
          statement.bind_int(0, kCurrentDBVersion);
          if (SQLITE_DONE != statement.step()) {
            return false;
          }
        }
  
        MakeSigninsTable(dbhandle.get());
        MakeCookiesTable(dbhandle.get());
        MakeClientIDTable(dbhandle.get());
      }
      transaction.Commit();
    }
  #if defined(OS_WIN)
    // Do not index this file. Scanning can occur every time we close the file,
    // which causes long delays in SQLite's file locking.
    const DWORD attrs = GetFileAttributes(settings_path.value().c_str());
    const BOOL attrs_set =
      SetFileAttributes(settings_path.value().c_str(),
                        attrs | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED);
  #endif
    return true;
  }
  
  UserSettings::~UserSettings() {
    if (dbhandle_)
      sqlite3_close(dbhandle_);
  }
  
  const int32 kInvalidHash = 0xFFFFFFFF;
  
  // We use 10 bits of data from the MD5 digest as the hash.
  const int32 kHashMask = 0x3FF;
  
  int32 GetHashFromDigest(const vector<uint8>& digest) {
    int32 hash = 0;
    int32 mask = kHashMask;
    for (vector<uint8>::const_iterator i = digest.begin(); i != digest.end();
         ++i) {
      hash = hash << 8;
      hash = hash | (*i & kHashMask);
      mask = mask >> 8;
      if (0 == mask)
        break;
    }
    return hash;
  }
  
  void UserSettings::StoreEmailForSignin(const string& signin,
                                         const string& primary_email) {
    ScopedDBHandle dbhandle(this);
    SQLTransaction transaction(dbhandle.get());
    int sqlite_result = transaction.BeginExclusive();
    CHECK(SQLITE_OK == sqlite_result);
    SQLStatement query;
    query.prepare(dbhandle.get(),
                  "SELECT COUNT(*) FROM signins"
                  " WHERE signin = ? AND primary_email = ?");
    query.bind_string(0, signin);
    query.bind_string(1, primary_email);
    int query_result = query.step();
    CHECK(SQLITE_ROW == query_result);
    int32 count = query.column_int(0);
    query.reset();
    if (0 == count) {
      // Migrate any settings the user might have from earlier versions.
      {
        SQLStatement statement;
        statement.prepare(dbhandle.get(),
                          "UPDATE settings SET email = ? WHERE email = ?");
        statement.bind_string(0, signin);
        statement.bind_string(1, primary_email);
        if (SQLITE_DONE != statement.step()) {
          LOG(FATAL) << sqlite3_errmsg(dbhandle.get());
        }
      }
      // Store this signin:email mapping.
      {
        SQLStatement statement;
        statement.prepare(dbhandle.get(),
                          "INSERT INTO signins(signin, primary_email)"
                          " values ( ?, ? )");
        statement.bind_string(0, signin);
        statement.bind_string(1, primary_email);
        if (SQLITE_DONE != statement.step()) {
          LOG(FATAL) << sqlite3_errmsg(dbhandle.get());
        }
      }
    }
    transaction.Commit();
  }
  
  // string* signin is both the input and the output of this function.
  bool UserSettings::GetEmailForSignin(string* signin) {
    ScopedDBHandle dbhandle(this);
    string result;
    SQLStatement query;
    query.prepare(dbhandle.get(),
                  "SELECT primary_email FROM signins WHERE signin = ?");
    query.bind_string(0, *signin);
    int query_result = query.step();
    if (SQLITE_ROW == query_result) {
      query.column_string(0, &result);
      if (!result.empty()) {
        swap(result, *signin);
        return true;
      }
    }
    return false;
  }
  
  void UserSettings::StoreHashedPassword(const string& email,
                                         const string& password) {
    // Save one-way hashed password:
    char binary_salt[kSaltSize];
    GetRandomBytes(binary_salt, sizeof(binary_salt));
  
    const string salt = APEncode(string(binary_salt, sizeof(binary_salt)));
    MD5Calculator md5;
    md5.AddData(salt.data(), salt.size());
    md5.AddData(password.data(), password.size());
    ScopedDBHandle dbhandle(this);
    SQLTransaction transaction(dbhandle.get());
    transaction.BeginExclusive();
    {
      SQLStatement statement;
      statement.prepare(dbhandle.get(),
                        "INSERT INTO settings(email, key, value)"
                        " values ( ?, ?, ? )");
      statement.bind_string(0, email);
      statement.bind_string(1, PASSWORD_HASH);
      statement.bind_int(2, GetHashFromDigest(md5.GetDigest()));
      if (SQLITE_DONE != statement.step()) {
        LOG(FATAL) << sqlite3_errmsg(dbhandle.get());
      }
    }
    {
      SQLStatement statement;
      statement.prepare(dbhandle.get(),
                        "INSERT INTO settings(email, key, value)"
                        " values ( ?, ?, ? )");
      statement.bind_string(0, email);
      statement.bind_string(1, SALT);
      statement.bind_string(2, salt);
      if (SQLITE_DONE != statement.step()) {
        LOG(FATAL) << sqlite3_errmsg(dbhandle.get());
      }
    }
    transaction.Commit();
  }
  
  bool UserSettings::VerifyAgainstStoredHash(const string& email,
                                             const string& password) {
    ScopedDBHandle dbhandle(this);
    string salt_and_digest;
  
    SQLStatement query;
    query.prepare(dbhandle.get(),
                  "SELECT key, value FROM settings"
                  " WHERE email = ? AND (key = ? OR key = ?)");
    query.bind_string(0, email);
    query.bind_string(1, PASSWORD_HASH);
    query.bind_string(2, SALT);
    int query_result = query.step();
    string salt;
    int32 hash = kInvalidHash;
    while (SQLITE_ROW == query_result) {
      string key(query.column_string(0));
      if (key == SALT)
        salt = query.column_string(1);
      else
        hash = query.column_int(1);
      query_result = query.step();
    }
    CHECK(SQLITE_DONE == query_result);
    if (salt.empty() || hash == kInvalidHash)
      return false;
    MD5Calculator md5;
    md5.AddData(salt.data(), salt.size());
    md5.AddData(password.data(), password.size());
    return hash == GetHashFromDigest(md5.GetDigest());
  }
  
  void UserSettings::SwitchUser(const string& username) {
    {
      base::AutoLock lock(mutex_);
      email_ = username;
    }
  }
  
  string UserSettings::GetClientId() {
    ScopedDBHandle dbhandle(this);
    SQLStatement statement;
    statement.prepare(dbhandle.get(), "SELECT id FROM client_id");
    int query_result = statement.step();
    string client_id;
    if (query_result == SQLITE_ROW)
      client_id = statement.column_string(0);
    return client_id;
  }
  
  void UserSettings::ClearAllServiceTokens() {
    ScopedDBHandle dbhandle(this);
    ExecOrDie(dbhandle.get(), "DELETE FROM cookies");
  }
  
  bool UserSettings::GetLastUser(string* username) {
    ScopedDBHandle dbhandle(this);
    SQLStatement query;
    query.prepare(dbhandle.get(), "SELECT email FROM cookies");
    if (SQLITE_ROW == query.step()) {
      *username = query.column_string(0);
      return true;
    } else {
      return false;
    }
  }
  
  }  // namespace browser_sync
  