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