OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2009 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 entry. |
| 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 #if defined(OS_WINDOWS) |
| 11 #include <windows.h> |
| 12 #endif |
| 13 |
| 14 #include <string> |
| 15 #include <limits> |
| 16 #include <vector> |
| 17 |
| 18 #include "base/file_util.h" |
| 19 #include "base/string_util.h" |
| 20 #include "chrome/browser/sync/syncable/directory_manager.h" // For migration. |
| 21 #include "chrome/browser/sync/util/crypto_helpers.h" |
| 22 #include "chrome/browser/sync/util/data_encryption.h" |
| 23 #include "chrome/browser/sync/util/path_helpers.h" |
| 24 #include "chrome/browser/sync/util/query_helpers.h" |
| 25 |
| 26 using std::numeric_limits; |
| 27 using std::string; |
| 28 using std::vector; |
| 29 |
| 30 using syncable::DirectoryManager; |
| 31 |
| 32 namespace browser_sync { |
| 33 |
| 34 static const char PASSWORD_HASH[] = "password_hash2"; |
| 35 static const char SALT[] = "salt2"; |
| 36 |
| 37 static const int kSaltSize = 20; |
| 38 static const int kCurrentDBVersion = 11; |
| 39 |
| 40 UserSettings::ScopedDBHandle::ScopedDBHandle(UserSettings* settings) : |
| 41 mutex_lock_(&settings->dbhandle_mutex_), handle_(&settings->dbhandle_) { |
| 42 } |
| 43 |
| 44 UserSettings::UserSettings() : |
| 45 dbhandle_(NULL) { |
| 46 } |
| 47 |
| 48 string UserSettings::email() const { |
| 49 ScopedLock lock(&mutex_); |
| 50 return email_; |
| 51 } |
| 52 |
| 53 static void MakeSigninsTable(sqlite3* const dbhandle) { |
| 54 // Multiple email addresses can map to the same Google Account. |
| 55 // This table keeps a map of sign-in email addresses to primary |
| 56 // Google Account email addresses. |
| 57 ExecOrDie(dbhandle, "CREATE TABLE signins" |
| 58 " (signin, primary_email, " |
| 59 " PRIMARY KEY(signin, primary_email) ON CONFLICT REPLACE)"); |
| 60 } |
| 61 |
| 62 void UserSettings::MigrateOldVersionsAsNeeded(sqlite3* const handle, |
| 63 int current_version) { |
| 64 switch (current_version) { |
| 65 // Versions 1-9 are unhandled. Version numbers greater than |
| 66 // kCurrentDBVersion should have already been weeded out by the caller. |
| 67 default: |
| 68 // When the version is too old, we just try to continue anyway. There |
| 69 // should not be a released product that makes a database too old for us |
| 70 // to handle. |
| 71 LOG(WARNING) << "UserSettings database version " << current_version << |
| 72 " is too old to handle."; |
| 73 return; |
| 74 case 10: |
| 75 { |
| 76 // Scrape the 'shares' table to find the syncable DB. 'shares' |
| 77 // had a pair of string columns that mapped the username to the filename |
| 78 // of the sync data sqlite3 file. Version 11 switched to a constant |
| 79 // filename, so here we read the string, copy the file to the new name, |
| 80 // delete the old one, and then drop the unused shares table. |
| 81 ScopedStatement share_query(PrepareQuery(handle, |
| 82 "SELECT share_name, file_name FROM shares")); |
| 83 int query_result = sqlite3_step(share_query.get()); |
| 84 CHECK(SQLITE_ROW == query_result); |
| 85 PathString share_name, file_name; |
| 86 GetColumn(share_query.get(), 0, &share_name); |
| 87 GetColumn(share_query.get(), 1, &file_name); |
| 88 |
| 89 if (!file_util::Move(file_name, |
| 90 DirectoryManager::GetSyncDataDatabaseFilename())) { |
| 91 LOG(WARNING) << "Unable to upgrade UserSettings from v10"; |
| 92 return; |
| 93 } |
| 94 } |
| 95 ExecOrDie(handle, "DROP TABLE shares"); |
| 96 ExecOrDie(handle, "UPDATE db_version SET version = 11"); |
| 97 // FALL THROUGH |
| 98 case kCurrentDBVersion: |
| 99 // Nothing to migrate. |
| 100 return; |
| 101 } |
| 102 } |
| 103 |
| 104 static void MakeCookiesTable(sqlite3* const dbhandle) { |
| 105 // This table keeps a list of auth tokens for each signed in account. There |
| 106 // will be as many rows as there are auth tokens per sign in. |
| 107 // The service_token column will store encrypted values. |
| 108 ExecOrDie(dbhandle, "CREATE TABLE cookies" |
| 109 " (email, service_name, service_token, " |
| 110 " PRIMARY KEY(email, service_name) ON CONFLICT REPLACE)"); |
| 111 } |
| 112 |
| 113 static void MakeSigninTypesTable(sqlite3* const dbhandle) { |
| 114 // With every successful gaia authentication, remember if it was |
| 115 // a hosted domain or not. |
| 116 ExecOrDie(dbhandle, "CREATE TABLE signin_types" |
| 117 " (signin, signin_type, " |
| 118 " PRIMARY KEY(signin, signin_type) ON CONFLICT REPLACE)"); |
| 119 } |
| 120 |
| 121 static void MakeClientIDTable(sqlite3* const dbhandle) { |
| 122 // Stores a single client ID value that can be used as the client id, |
| 123 // if there's not another such ID provided on the install. |
| 124 ExecOrDie(dbhandle, "CREATE TABLE client_id (id) "); |
| 125 ExecOrDie(dbhandle, "INSERT INTO client_id values ( ? )", |
| 126 Generate128BitRandomHexString()); |
| 127 } |
| 128 |
| 129 bool UserSettings::Init(const PathString& settings_path) { |
| 130 { // Scope the handle |
| 131 ScopedDBHandle dbhandle(this); |
| 132 if (dbhandle_) |
| 133 sqlite3_close(dbhandle_); |
| 134 CHECK(SQLITE_OK == SqliteOpen(settings_path.c_str(), &dbhandle_)); |
| 135 // In the worst case scenario, the user may hibernate his computer during |
| 136 // one of our transactions. |
| 137 sqlite3_busy_timeout(dbhandle_, numeric_limits<int>::max()); |
| 138 |
| 139 int sqlite_result = Exec(dbhandle.get(), "BEGIN EXCLUSIVE TRANSACTION"); |
| 140 CHECK(SQLITE_DONE == sqlite_result); |
| 141 ScopedStatement table_query(PrepareQuery(dbhandle.get(), |
| 142 "select count(*) from sqlite_master where type = 'table'" |
| 143 " and name = 'db_version'")); |
| 144 int query_result = sqlite3_step(table_query.get()); |
| 145 CHECK(SQLITE_ROW == query_result); |
| 146 int table_count = 0; |
| 147 GetColumn(table_query.get(), 0, &table_count); |
| 148 table_query.reset(NULL); |
| 149 if (table_count > 0) { |
| 150 ScopedStatement version_query(PrepareQuery(dbhandle.get(), |
| 151 "SELECT version FROM db_version")); |
| 152 query_result = sqlite3_step(version_query.get()); |
| 153 CHECK(SQLITE_ROW == query_result); |
| 154 const int version = sqlite3_column_int(version_query.get(), 0); |
| 155 version_query.reset(NULL); |
| 156 if (version > kCurrentDBVersion) { |
| 157 LOG(WARNING) << "UserSettings database is too new."; |
| 158 return false; |
| 159 } |
| 160 |
| 161 MigrateOldVersionsAsNeeded(dbhandle.get(), version); |
| 162 } else { |
| 163 // Create settings table. |
| 164 ExecOrDie(dbhandle.get(), "CREATE TABLE settings" |
| 165 " (email, key, value, " |
| 166 " PRIMARY KEY(email, key) ON CONFLICT REPLACE)"); |
| 167 |
| 168 // Create and populate version table. |
| 169 ExecOrDie(dbhandle.get(), "CREATE TABLE db_version ( version )"); |
| 170 ExecOrDie(dbhandle.get(), "INSERT INTO db_version values ( ? )", |
| 171 kCurrentDBVersion); |
| 172 |
| 173 MakeSigninsTable(dbhandle.get()); |
| 174 MakeCookiesTable(dbhandle.get()); |
| 175 MakeSigninTypesTable(dbhandle.get()); |
| 176 MakeClientIDTable(dbhandle.get()); |
| 177 } |
| 178 ExecOrDie(dbhandle.get(), "COMMIT TRANSACTION"); |
| 179 } |
| 180 #ifdef OS_WINDOWS |
| 181 // Do not index this file. Scanning can occur every time we close the file, |
| 182 // which causes long delays in SQLite's file locking. |
| 183 const DWORD attrs = GetFileAttributes(settings_path.c_str()); |
| 184 const BOOL attrs_set = |
| 185 SetFileAttributes(settings_path.c_str(), |
| 186 attrs | FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); |
| 187 #endif |
| 188 return true; |
| 189 } |
| 190 |
| 191 |
| 192 UserSettings::~UserSettings() { |
| 193 if (dbhandle_) |
| 194 sqlite3_close(dbhandle_); |
| 195 } |
| 196 |
| 197 const int32 kInvalidHash = 0xFFFFFFFF; |
| 198 |
| 199 // We use 10 bits of data from the MD5 digest as the hash. |
| 200 const int32 kHashMask = 0x3FF; |
| 201 |
| 202 int32 GetHashFromDigest(const vector<uint8>& digest) { |
| 203 int32 hash = 0; |
| 204 int32 mask = kHashMask; |
| 205 for (vector<uint8>::const_iterator i = digest.begin(); i != digest.end(); |
| 206 ++i) { |
| 207 hash = hash << 8; |
| 208 hash = hash | (*i & kHashMask); |
| 209 mask = mask >> 8; |
| 210 if (0 == mask) |
| 211 break; |
| 212 } |
| 213 return hash; |
| 214 } |
| 215 |
| 216 void UserSettings::StoreEmailForSignin(const string& signin, |
| 217 const string& primary_email) { |
| 218 ScopedDBHandle dbhandle(this); |
| 219 ExecOrDie(dbhandle.get(), "BEGIN TRANSACTION"); |
| 220 ScopedStatement query(PrepareQuery(dbhandle.get(), |
| 221 "SELECT COUNT(*) FROM signins" |
| 222 " WHERE signin = ? AND primary_email = ?", |
| 223 signin, primary_email)); |
| 224 int query_result = sqlite3_step(query.get()); |
| 225 CHECK(SQLITE_ROW == query_result); |
| 226 int32 count = 0; |
| 227 GetColumn(query.get(), 0, &count); |
| 228 query.reset(NULL); |
| 229 if (0 == count) { |
| 230 // Migrate any settings the user might have from earlier versions. |
| 231 ExecOrDie(dbhandle.get(), "UPDATE settings SET email = ? WHERE email = ?", |
| 232 primary_email, signin); |
| 233 // Store this signin:email mapping. |
| 234 ExecOrDie(dbhandle.get(), "INSERT INTO signins(signin, primary_email)" |
| 235 " values ( ?, ? )", signin, primary_email); |
| 236 } |
| 237 ExecOrDie(dbhandle.get(), "COMMIT TRANSACTION"); |
| 238 } |
| 239 |
| 240 bool UserSettings::GetEmailForSignin(/*in, out*/string* signin) { |
| 241 ScopedDBHandle dbhandle(this); |
| 242 string result; |
| 243 ScopedStatement query(PrepareQuery(dbhandle.get(), |
| 244 "SELECT primary_email FROM signins" |
| 245 " WHERE signin = ?", *signin)); |
| 246 int query_result = sqlite3_step(query.get()); |
| 247 if (SQLITE_ROW == query_result) { |
| 248 GetColumn(query.get(), 0, &result); |
| 249 if (!result.empty()) { |
| 250 swap(result, *signin); |
| 251 return true; |
| 252 } |
| 253 } |
| 254 return false; |
| 255 } |
| 256 |
| 257 void UserSettings::StoreHashedPassword(const string& email, |
| 258 const string& password) { |
| 259 // Save one-way hashed password: |
| 260 char binary_salt[kSaltSize]; |
| 261 { |
| 262 ScopedLock lock(&mutex_); |
| 263 GetRandomBytes(binary_salt, sizeof(binary_salt)); |
| 264 } |
| 265 const string salt = APEncode(string(binary_salt, sizeof(binary_salt))); |
| 266 MD5Calculator md5; |
| 267 md5.AddData(salt.data(), salt.size()); |
| 268 md5.AddData(password.data(), password.size()); |
| 269 ScopedDBHandle dbhandle(this); |
| 270 ExecOrDie(dbhandle.get(), "BEGIN TRANSACTION"); |
| 271 ExecOrDie(dbhandle.get(), "INSERT INTO settings(email, key, value )" |
| 272 " values ( ?, ?, ? )", email, PASSWORD_HASH, |
| 273 GetHashFromDigest(md5.GetDigest())); |
| 274 ExecOrDie(dbhandle.get(), "INSERT INTO settings(email, key, value )" |
| 275 " values ( ?, ?, ? )", email, SALT, salt); |
| 276 ExecOrDie(dbhandle.get(), "COMMIT TRANSACTION"); |
| 277 } |
| 278 |
| 279 bool UserSettings::VerifyAgainstStoredHash(const string& email, |
| 280 const string& password) { |
| 281 ScopedDBHandle dbhandle(this); |
| 282 string salt_and_digest; |
| 283 |
| 284 ScopedStatement query(PrepareQuery(dbhandle.get(), |
| 285 "SELECT key, value FROM settings" |
| 286 " WHERE email = ? AND" |
| 287 " (key = ? OR key = ?)", |
| 288 email, PASSWORD_HASH, SALT)); |
| 289 int query_result = sqlite3_step(query.get()); |
| 290 string salt; |
| 291 int32 hash = kInvalidHash; |
| 292 while (SQLITE_ROW == query_result) { |
| 293 string key; |
| 294 GetColumn(query.get(), 0, &key); |
| 295 if (key == SALT) |
| 296 GetColumn(query.get(), 1, &salt); |
| 297 else |
| 298 GetColumn(query.get(), 1, &hash); |
| 299 query_result = sqlite3_step(query.get()); |
| 300 } |
| 301 CHECK(SQLITE_DONE == query_result); |
| 302 if (salt.empty() || hash == kInvalidHash) |
| 303 return false; |
| 304 MD5Calculator md5; |
| 305 md5.AddData(salt.data(), salt.size()); |
| 306 md5.AddData(password.data(), password.size()); |
| 307 return hash == GetHashFromDigest(md5.GetDigest()); |
| 308 } |
| 309 |
| 310 void UserSettings::SwitchUser(const string& username) { |
| 311 { |
| 312 ScopedLock lock(&mutex_); |
| 313 email_ = username; |
| 314 } |
| 315 } |
| 316 |
| 317 void UserSettings::RememberSigninType(const string& signin, SignIn signin_type) |
| 318 { |
| 319 ScopedDBHandle dbhandle(this); |
| 320 ExecOrDie(dbhandle.get(), "INSERT INTO signin_types(signin, signin_type)" |
| 321 " values ( ?, ? )", signin, static_cast<int>(signin_type)); |
| 322 } |
| 323 |
| 324 SignIn UserSettings::RecallSigninType(const string& signin, SignIn default_type) |
| 325 { |
| 326 ScopedDBHandle dbhandle(this); |
| 327 ScopedStatement query(PrepareQuery(dbhandle.get(), |
| 328 "SELECT signin_type from signin_types" |
| 329 " WHERE signin = ?", signin)); |
| 330 int query_result = sqlite3_step(query.get()); |
| 331 if (SQLITE_ROW == query_result) { |
| 332 int signin_type; |
| 333 GetColumn(query.get(), 0, &signin_type); |
| 334 return static_cast<SignIn>(signin_type); |
| 335 } |
| 336 return default_type; |
| 337 } |
| 338 |
| 339 string UserSettings::GetClientId() { |
| 340 ScopedDBHandle dbhandle(this); |
| 341 ScopedStatement query(PrepareQuery(dbhandle.get(), |
| 342 "SELECT id FROM client_id")); |
| 343 int query_result = sqlite3_step(query.get()); |
| 344 string client_id; |
| 345 if (query_result == SQLITE_ROW) |
| 346 GetColumn(query.get(), 0, &client_id); |
| 347 return client_id; |
| 348 } |
| 349 |
| 350 } // namespace browser_sync |
OLD | NEW |