• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ExecOrDie(sqlite3 * dbhandle,const char * query)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.
APEncode(const string & in)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 
APDecode(const string & in)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 
ScopedDBHandle(UserSettings * settings)74 UserSettings::ScopedDBHandle::ScopedDBHandle(UserSettings* settings)
75     : mutex_lock_(settings->dbhandle_mutex_), handle_(&settings->dbhandle_) {
76 }
77 
UserSettings()78 UserSettings::UserSettings() : dbhandle_(NULL) {
79 }
80 
email() const81 string UserSettings::email() const {
82   base::AutoLock lock(mutex_);
83   return email_;
84 }
85 
MakeSigninsTable(sqlite3 * const dbhandle)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 
MigrateOldVersionsAsNeeded(sqlite3 * const handle,int current_version)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 
MakeCookiesTable(sqlite3 * const dbhandle)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 
MakeClientIDTable(sqlite3 * const dbhandle)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 
Init(const FilePath & settings_path)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 
~UserSettings()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 
GetHashFromDigest(const vector<uint8> & digest)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 
StoreEmailForSignin(const string & signin,const string & primary_email)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.
GetEmailForSignin(string * signin)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 
StoreHashedPassword(const string & email,const string & password)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 
VerifyAgainstStoredHash(const string & email,const string & password)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 
SwitchUser(const string & username)422 void UserSettings::SwitchUser(const string& username) {
423   {
424     base::AutoLock lock(mutex_);
425     email_ = username;
426   }
427 }
428 
GetClientId()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 
ClearAllServiceTokens()440 void UserSettings::ClearAllServiceTokens() {
441   ScopedDBHandle dbhandle(this);
442   ExecOrDie(dbhandle.get(), "DELETE FROM cookies");
443 }
444 
GetLastUser(string * username)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