// 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