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
27using std::numeric_limits;
28using std::string;
29using std::vector;
30
31using syncable::DirectoryManager;
32
33namespace browser_sync {
34
35void 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.
45string 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
56string 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
68static const char PASSWORD_HASH[] = "password_hash2";
69static const char SALT[] = "salt2";
70
71static const int kSaltSize = 20;
72static const int kCurrentDBVersion = 12;
73
74UserSettings::ScopedDBHandle::ScopedDBHandle(UserSettings* settings)
75    : mutex_lock_(settings->dbhandle_mutex_), handle_(&settings->dbhandle_) {
76}
77
78UserSettings::UserSettings() : dbhandle_(NULL) {
79}
80
81string UserSettings::email() const {
82  base::AutoLock lock(mutex_);
83  return email_;
84}
85
86static 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
96void 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
151static 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
161static 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
176bool 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
263UserSettings::~UserSettings() {
264  if (dbhandle_)
265    sqlite3_close(dbhandle_);
266}
267
268const int32 kInvalidHash = 0xFFFFFFFF;
269
270// We use 10 bits of data from the MD5 digest as the hash.
271const int32 kHashMask = 0x3FF;
272
273int32 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
287void 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.
332bool 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
350void 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
390bool 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
422void UserSettings::SwitchUser(const string& username) {
423  {
424    base::AutoLock lock(mutex_);
425    email_ = username;
426  }
427}
428
429string 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
440void UserSettings::ClearAllServiceTokens() {
441  ScopedDBHandle dbhandle(this);
442  ExecOrDie(dbhandle.get(), "DELETE FROM cookies");
443}
444
445bool 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