1 // Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 // 5 // This class isn't pretty. It's just a step better than globals, which is what 6 // these were previously. 7 8 #include "chrome/browser/sync/util/user_settings.h" 9 10 #include "build/build_config.h" 11 12 #if defined(OS_WIN) 13 #include <windows.h> 14 #endif 15 16 #include <limits> 17 #include <string> 18 #include <vector> 19 20 #include "base/file_util.h" 21 #include "base/string_util.h" 22 #include "chrome/browser/sync/syncable/directory_manager.h" // For migration. 23 #include "chrome/browser/sync/util/crypto_helpers.h" 24 #include "chrome/browser/sync/util/data_encryption.h" 25 #include "chrome/common/sqlite_utils.h" 26 27 using std::numeric_limits; 28 using std::string; 29 using std::vector; 30 31 using syncable::DirectoryManager; 32 33 namespace browser_sync { 34 35 void ExecOrDie(sqlite3* dbhandle, const char *query) { 36 SQLStatement statement; 37 statement.prepare(dbhandle, query); 38 if (SQLITE_DONE != statement.step()) { 39 LOG(FATAL) << query << "\n" << sqlite3_errmsg(dbhandle); 40 } 41 } 42 43 // Useful for encoding any sequence of bytes into a string that can be used in 44 // a table name. Kind of like hex encoding, except that A is zero and P is 15. 45 string APEncode(const string& in) { 46 string result; 47 result.reserve(in.size() * 2); 48 for (string::const_iterator i = in.begin(); i != in.end(); ++i) { 49 unsigned int c = static_cast<unsigned char>(*i); 50 result.push_back((c & 0x0F) + 'A'); 51 result.push_back(((c >> 4) & 0x0F) + 'A'); 52 } 53 return result; 54 } 55 56 string APDecode(const string& in) { 57 string result; 58 result.reserve(in.size() / 2); 59 for (string::const_iterator i = in.begin(); i != in.end(); ++i) { 60 unsigned int c = *i - 'A'; 61 if (++i != in.end()) 62 c = c | (static_cast<unsigned char>(*i - 'A') << 4); 63 result.push_back(c); 64 } 65 return result; 66 } 67 68 static const char PASSWORD_HASH[] = "password_hash2"; 69 static const char SALT[] = "salt2"; 70 71 static const int kSaltSize = 20; 72 static const int kCurrentDBVersion = 12; 73 74 UserSettings::ScopedDBHandle::ScopedDBHandle(UserSettings* settings) 75 : mutex_lock_(settings->dbhandle_mutex_), handle_(&settings->dbhandle_) { 76 } 77 78 UserSettings::UserSettings() : dbhandle_(NULL) { 79 } 80 81 string UserSettings::email() const { 82 base::AutoLock lock(mutex_); 83 return email_; 84 } 85 86 static void MakeSigninsTable(sqlite3* const dbhandle) { 87 // Multiple email addresses can map to the same Google Account. This table 88 // keeps a map of sign-in email addresses to primary Google Account email 89 // addresses. 90 ExecOrDie(dbhandle, 91 "CREATE TABLE signins" 92 " (signin, primary_email, " 93 " PRIMARY KEY(signin, primary_email) ON CONFLICT REPLACE)"); 94 } 95 96 void UserSettings::MigrateOldVersionsAsNeeded(sqlite3* const handle, 97 int current_version) { 98 switch (current_version) { 99 // Versions 1-9 are unhandled. Version numbers greater than 100 // kCurrentDBVersion should have already been weeded out by the caller. 101 default: 102 // When the version is too old, we just try to continue anyway. There 103 // should not be a released product that makes a database too old for us 104 // to handle. 105 LOG(WARNING) << "UserSettings database version " << current_version << 106 " is too old to handle."; 107 return; 108 case 10: 109 { 110 // Scrape the 'shares' table to find the syncable DB. 'shares' had a 111 // pair of string columns that mapped the username to the filename of 112 // the sync data sqlite3 file. Version 11 switched to a constant 113 // filename, so here we read the string, copy the file to the new name, 114 // delete the old one, and then drop the unused shares table. 115 SQLStatement share_query; 116 share_query.prepare(handle, "SELECT share_name, file_name FROM shares"); 117 int query_result = share_query.step(); 118 CHECK(SQLITE_ROW == query_result); 119 FilePath::StringType share_name, file_name; 120 #if defined(OS_POSIX) 121 share_name = share_query.column_string(0); 122 file_name = share_query.column_string(1); 123 #else 124 share_name = share_query.column_wstring(0); 125 file_name = share_query.column_wstring(1); 126 #endif 127 128 const FilePath& src_syncdata_path = FilePath(file_name); 129 FilePath dst_syncdata_path(src_syncdata_path.DirName()); 130 file_util::AbsolutePath(&dst_syncdata_path); 131 dst_syncdata_path = dst_syncdata_path.Append( 132 DirectoryManager::GetSyncDataDatabaseFilename()); 133 if (!file_util::Move(src_syncdata_path, dst_syncdata_path)) { 134 LOG(WARNING) << "Unable to upgrade UserSettings from v10"; 135 return; 136 } 137 } 138 ExecOrDie(handle, "DROP TABLE shares"); 139 ExecOrDie(handle, "UPDATE db_version SET version = 11"); 140 // FALL THROUGH 141 case 11: 142 ExecOrDie(handle, "DROP TABLE signin_types"); 143 ExecOrDie(handle, "UPDATE db_version SET version = 12"); 144 // FALL THROUGH 145 case kCurrentDBVersion: 146 // Nothing to migrate. 147 return; 148 } 149 } 150 151 static void MakeCookiesTable(sqlite3* const dbhandle) { 152 // This table keeps a list of auth tokens for each signed in account. There 153 // will be as many rows as there are auth tokens per sign in. 154 // The service_token column will store encrypted values. 155 ExecOrDie(dbhandle, 156 "CREATE TABLE cookies" 157 " (email, service_name, service_token, " 158 " PRIMARY KEY(email, service_name) ON CONFLICT REPLACE)"); 159 } 160 161 static void MakeClientIDTable(sqlite3* const dbhandle) { 162 // Stores a single client ID value that can be used as the client id, if 163 // there's not another such ID provided on the install. 164 ExecOrDie(dbhandle, "CREATE TABLE client_id (id) "); 165 { 166 SQLStatement statement; 167 statement.prepare(dbhandle, 168 "INSERT INTO client_id values ( ? )"); 169 statement.bind_string(0, Generate128BitRandomHexString()); 170 if (SQLITE_DONE != statement.step()) { 171 LOG(FATAL) << "INSERT INTO client_id\n" << sqlite3_errmsg(dbhandle); 172 } 173 } 174 } 175 176 bool UserSettings::Init(const FilePath& settings_path) { 177 { // Scope the handle. 178 ScopedDBHandle dbhandle(this); 179 if (dbhandle_) 180 sqlite3_close(dbhandle_); 181 182 if (SQLITE_OK != sqlite_utils::OpenSqliteDb(settings_path, &dbhandle_)) 183 return false; 184 185 // In the worst case scenario, the user may hibernate his computer during 186 // one of our transactions. 187 sqlite3_busy_timeout(dbhandle_, numeric_limits<int>::max()); 188 ExecOrDie(dbhandle.get(), "PRAGMA fullfsync = 1"); 189 ExecOrDie(dbhandle.get(), "PRAGMA synchronous = 2"); 190 191 SQLTransaction transaction(dbhandle.get()); 192 transaction.BeginExclusive(); 193 SQLStatement table_query; 194 table_query.prepare(dbhandle.get(), 195 "select count(*) from sqlite_master" 196 " where type = 'table' and name = 'db_version'"); 197 int query_result = table_query.step(); 198 CHECK(SQLITE_ROW == query_result); 199 int table_count = table_query.column_int(0); 200 table_query.reset(); 201 if (table_count > 0) { 202 SQLStatement version_query; 203 version_query.prepare(dbhandle.get(), 204 "SELECT version FROM db_version"); 205 query_result = version_query.step(); 206 CHECK(SQLITE_ROW == query_result); 207 const int version = version_query.column_int(0); 208 version_query.reset(); 209 if (version > kCurrentDBVersion) { 210 LOG(WARNING) << "UserSettings database is too new."; 211 return false; 212 } 213 214 MigrateOldVersionsAsNeeded(dbhandle.get(), version); 215 } else { 216 // Create settings table. 217 { 218 SQLStatement statement; 219 statement.prepare(dbhandle.get(), 220 "CREATE TABLE settings" 221 " (email, key, value, " 222 " PRIMARY KEY(email, key) ON CONFLICT REPLACE)"); 223 if (SQLITE_DONE != statement.step()) { 224 return false; 225 } 226 } 227 // Create and populate version table. 228 { 229 SQLStatement statement; 230 statement.prepare(dbhandle.get(), 231 "CREATE TABLE db_version ( version )"); 232 if (SQLITE_DONE != statement.step()) { 233 return false; 234 } 235 } 236 { 237 SQLStatement statement; 238 statement.prepare(dbhandle.get(), 239 "INSERT INTO db_version values ( ? )"); 240 statement.bind_int(0, kCurrentDBVersion); 241 if (SQLITE_DONE != statement.step()) { 242 return false; 243 } 244 } 245 246 MakeSigninsTable(dbhandle.get()); 247 MakeCookiesTable(dbhandle.get()); 248 MakeClientIDTable(dbhandle.get()); 249 } 250 transaction.Commit(); 251 } 252 #if defined(OS_WIN) 253 // Do not index this file. Scanning can occur every time we close the file, 254 // which causes long delays in SQLite's file locking. 255 const DWORD attrs = GetFileAttributes(settings_path.value().c_str()); 256 const BOOL attrs_set = 257 SetFileAttributes(settings_path.value().c_str(), 258 attrs | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); 259 #endif 260 return true; 261 } 262 263 UserSettings::~UserSettings() { 264 if (dbhandle_) 265 sqlite3_close(dbhandle_); 266 } 267 268 const int32 kInvalidHash = 0xFFFFFFFF; 269 270 // We use 10 bits of data from the MD5 digest as the hash. 271 const int32 kHashMask = 0x3FF; 272 273 int32 GetHashFromDigest(const vector<uint8>& digest) { 274 int32 hash = 0; 275 int32 mask = kHashMask; 276 for (vector<uint8>::const_iterator i = digest.begin(); i != digest.end(); 277 ++i) { 278 hash = hash << 8; 279 hash = hash | (*i & kHashMask); 280 mask = mask >> 8; 281 if (0 == mask) 282 break; 283 } 284 return hash; 285 } 286 287 void UserSettings::StoreEmailForSignin(const string& signin, 288 const string& primary_email) { 289 ScopedDBHandle dbhandle(this); 290 SQLTransaction transaction(dbhandle.get()); 291 int sqlite_result = transaction.BeginExclusive(); 292 CHECK(SQLITE_OK == sqlite_result); 293 SQLStatement query; 294 query.prepare(dbhandle.get(), 295 "SELECT COUNT(*) FROM signins" 296 " WHERE signin = ? AND primary_email = ?"); 297 query.bind_string(0, signin); 298 query.bind_string(1, primary_email); 299 int query_result = query.step(); 300 CHECK(SQLITE_ROW == query_result); 301 int32 count = query.column_int(0); 302 query.reset(); 303 if (0 == count) { 304 // Migrate any settings the user might have from earlier versions. 305 { 306 SQLStatement statement; 307 statement.prepare(dbhandle.get(), 308 "UPDATE settings SET email = ? WHERE email = ?"); 309 statement.bind_string(0, signin); 310 statement.bind_string(1, primary_email); 311 if (SQLITE_DONE != statement.step()) { 312 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); 313 } 314 } 315 // Store this signin:email mapping. 316 { 317 SQLStatement statement; 318 statement.prepare(dbhandle.get(), 319 "INSERT INTO signins(signin, primary_email)" 320 " values ( ?, ? )"); 321 statement.bind_string(0, signin); 322 statement.bind_string(1, primary_email); 323 if (SQLITE_DONE != statement.step()) { 324 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); 325 } 326 } 327 } 328 transaction.Commit(); 329 } 330 331 // string* signin is both the input and the output of this function. 332 bool UserSettings::GetEmailForSignin(string* signin) { 333 ScopedDBHandle dbhandle(this); 334 string result; 335 SQLStatement query; 336 query.prepare(dbhandle.get(), 337 "SELECT primary_email FROM signins WHERE signin = ?"); 338 query.bind_string(0, *signin); 339 int query_result = query.step(); 340 if (SQLITE_ROW == query_result) { 341 query.column_string(0, &result); 342 if (!result.empty()) { 343 swap(result, *signin); 344 return true; 345 } 346 } 347 return false; 348 } 349 350 void UserSettings::StoreHashedPassword(const string& email, 351 const string& password) { 352 // Save one-way hashed password: 353 char binary_salt[kSaltSize]; 354 GetRandomBytes(binary_salt, sizeof(binary_salt)); 355 356 const string salt = APEncode(string(binary_salt, sizeof(binary_salt))); 357 MD5Calculator md5; 358 md5.AddData(salt.data(), salt.size()); 359 md5.AddData(password.data(), password.size()); 360 ScopedDBHandle dbhandle(this); 361 SQLTransaction transaction(dbhandle.get()); 362 transaction.BeginExclusive(); 363 { 364 SQLStatement statement; 365 statement.prepare(dbhandle.get(), 366 "INSERT INTO settings(email, key, value)" 367 " values ( ?, ?, ? )"); 368 statement.bind_string(0, email); 369 statement.bind_string(1, PASSWORD_HASH); 370 statement.bind_int(2, GetHashFromDigest(md5.GetDigest())); 371 if (SQLITE_DONE != statement.step()) { 372 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); 373 } 374 } 375 { 376 SQLStatement statement; 377 statement.prepare(dbhandle.get(), 378 "INSERT INTO settings(email, key, value)" 379 " values ( ?, ?, ? )"); 380 statement.bind_string(0, email); 381 statement.bind_string(1, SALT); 382 statement.bind_string(2, salt); 383 if (SQLITE_DONE != statement.step()) { 384 LOG(FATAL) << sqlite3_errmsg(dbhandle.get()); 385 } 386 } 387 transaction.Commit(); 388 } 389 390 bool UserSettings::VerifyAgainstStoredHash(const string& email, 391 const string& password) { 392 ScopedDBHandle dbhandle(this); 393 string salt_and_digest; 394 395 SQLStatement query; 396 query.prepare(dbhandle.get(), 397 "SELECT key, value FROM settings" 398 " WHERE email = ? AND (key = ? OR key = ?)"); 399 query.bind_string(0, email); 400 query.bind_string(1, PASSWORD_HASH); 401 query.bind_string(2, SALT); 402 int query_result = query.step(); 403 string salt; 404 int32 hash = kInvalidHash; 405 while (SQLITE_ROW == query_result) { 406 string key(query.column_string(0)); 407 if (key == SALT) 408 salt = query.column_string(1); 409 else 410 hash = query.column_int(1); 411 query_result = query.step(); 412 } 413 CHECK(SQLITE_DONE == query_result); 414 if (salt.empty() || hash == kInvalidHash) 415 return false; 416 MD5Calculator md5; 417 md5.AddData(salt.data(), salt.size()); 418 md5.AddData(password.data(), password.size()); 419 return hash == GetHashFromDigest(md5.GetDigest()); 420 } 421 422 void UserSettings::SwitchUser(const string& username) { 423 { 424 base::AutoLock lock(mutex_); 425 email_ = username; 426 } 427 } 428 429 string UserSettings::GetClientId() { 430 ScopedDBHandle dbhandle(this); 431 SQLStatement statement; 432 statement.prepare(dbhandle.get(), "SELECT id FROM client_id"); 433 int query_result = statement.step(); 434 string client_id; 435 if (query_result == SQLITE_ROW) 436 client_id = statement.column_string(0); 437 return client_id; 438 } 439 440 void UserSettings::ClearAllServiceTokens() { 441 ScopedDBHandle dbhandle(this); 442 ExecOrDie(dbhandle.get(), "DELETE FROM cookies"); 443 } 444 445 bool UserSettings::GetLastUser(string* username) { 446 ScopedDBHandle dbhandle(this); 447 SQLStatement query; 448 query.prepare(dbhandle.get(), "SELECT email FROM cookies"); 449 if (SQLITE_ROW == query.step()) { 450 *username = query.column_string(0); 451 return true; 452 } else { 453 return false; 454 } 455 } 456 457 } // namespace browser_sync 458