Android Open Source - google-authenticator-android Account Db






From Project

Back to project page google-authenticator-android.

License

The source code is released under:

Apache License

If you think the Android project google-authenticator-android listed in this page is inappropriate, such as containing malicious code/tools or violating the copyright, please email info at java2s dot com, thanks.

Java Source Code

/*
 * Copyright 2010 Google Inc. All Rights Reserved.
 *//w  w w .ja v  a  2s  .  c o  m
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.apps.authenticator;

import com.google.android.apps.authenticator.Base32String.DecodingException;
import com.google.android.apps.authenticator.PasscodeGenerator.Signer;

import android.content.ContentValues;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Process;
import android.util.Log;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

/**
 * A database of email addresses and secret values
 *
 * @author sweis@google.com (Steve Weis)
 */
public class AccountDb {
  public static final Integer DEFAULT_HOTP_COUNTER = 0;

  public static final String GOOGLE_CORP_ACCOUNT_NAME = "Google Internal 2Factor";

  private static final String ID_COLUMN = "_id";
  private static final String EMAIL_COLUMN = "email";
  private static final String SECRET_COLUMN = "secret";
  private static final String COUNTER_COLUMN = "counter";
  private static final String TYPE_COLUMN = "type";
  // @VisibleForTesting
  static final String PROVIDER_COLUMN = "provider";
  // @VisibleForTesting
  static final String TABLE_NAME = "accounts";
  // @VisibleForTesting
  static final String PATH = "databases";

  private static final String TABLE_INFO_COLUMN_NAME_COLUMN = "name";

  private static final int PROVIDER_UNKNOWN = 0;
  private static final int PROVIDER_GOOGLE = 1;

  // @VisibleForTesting
  SQLiteDatabase mDatabase;

  private static final String LOCAL_TAG = "GoogleAuthenticator.AccountDb";

  /**
   * Types of secret keys.
   */
  public enum OtpType {  // must be the same as in res/values/strings.xml:type
    TOTP (0),  // time based
    HOTP (1);  // counter based

    public final Integer value;  // value as stored in SQLite database
    OtpType(Integer value) {
      this.value = value;
    }

    public static OtpType getEnum(Integer i) {
      for (OtpType type : OtpType.values()) {
        if (type.value.equals(i)) {
          return type;
        }
      }

      return null;
    }

  }

  public AccountDb(Context context) {
    mDatabase = openDatabase(context);

    // Create the table if it doesn't exist
    mDatabase.execSQL(String.format(
        "CREATE TABLE IF NOT EXISTS %s" +
        " (%s INTEGER PRIMARY KEY, %s TEXT NOT NULL, %s TEXT NOT NULL, " +
        " %s INTEGER DEFAULT %s, %s INTEGER, %s INTEGER DEFAULT %s)",
        TABLE_NAME, ID_COLUMN, EMAIL_COLUMN, SECRET_COLUMN, COUNTER_COLUMN,
        DEFAULT_HOTP_COUNTER, TYPE_COLUMN,
        PROVIDER_COLUMN, PROVIDER_UNKNOWN));

    Collection<String> tableColumnNames = listTableColumnNamesLowerCase();
    if (!tableColumnNames.contains(PROVIDER_COLUMN.toLowerCase(Locale.US))) {
      // Migrate from old schema where the PROVIDER_COLUMN wasn't there
      mDatabase.execSQL(String.format(
          "ALTER TABLE %s ADD COLUMN %s INTEGER DEFAULT %s",
          TABLE_NAME, PROVIDER_COLUMN, PROVIDER_UNKNOWN));
    }
  }

  /*
   * Tries three times to open database before throwing AccountDbOpenException.
   */
  private SQLiteDatabase openDatabase(Context context) {
    for (int count = 0; true; count++) {
      try {
        return context.openOrCreateDatabase(PATH, Context.MODE_PRIVATE, null);
      } catch (SQLiteException e) {
        if (count < 2) {
          continue;
        } else {
          throw new AccountDbOpenException("Failed to open AccountDb database in three tries.\n"
              + getAccountDbOpenFailedErrorString(context), e);
        }
      }
    }
  }

  private String getAccountDbOpenFailedErrorString(Context context) {
    String dataPackageDir = context.getApplicationInfo().dataDir;
    String databaseDirPathname = context.getDatabasePath(PATH).getParent();
    String databasePathname = context.getDatabasePath(PATH).getAbsolutePath();
    String[] dirsToStat = new String[] {dataPackageDir, databaseDirPathname, databasePathname};
    StringBuilder error = new StringBuilder();
    int myUid = Process.myUid();
    for (String directory : dirsToStat) {
      try {
        FileUtilities.StatStruct stat = FileUtilities.getStat(directory);
        String ownerUidName = null;
        try {
          if (stat.uid == 0) {
            ownerUidName = "root";
          } else {
            PackageManager packageManager = context.getPackageManager();
            ownerUidName = (packageManager != null) ? packageManager.getNameForUid(stat.uid) : null;
          }
        } catch (Exception e) {
          ownerUidName = e.toString();
        }
        error.append(directory + " directory stat (my UID: " + myUid);
        if (ownerUidName == null) {
          error.append("): ");
        } else {
          error.append(", dir owner UID name: " + ownerUidName + "): ");
        }
        error.append(stat.toString() + "\n");
      } catch (IOException e) {
        error.append(directory + " directory stat threw an exception: " + e + "\n");
      }
    }
    return error.toString();
  }

  /**
   * Closes this database and releases any system resources held.
   */
  public void close() {
    mDatabase.close();
  }

  /**
   * Lists the names of all the columns in the specified table.
   */
  // @VisibleForTesting
  static Collection<String> listTableColumnNamesLowerCase(
      SQLiteDatabase database, String tableName) {
    Cursor cursor =
        database.rawQuery(String.format("PRAGMA table_info(%s)", tableName), new String[0]);
    Collection<String> result = new ArrayList<String>();
    try {
      if (cursor != null) {
        int nameColumnIndex = cursor.getColumnIndexOrThrow(TABLE_INFO_COLUMN_NAME_COLUMN);
        while (cursor.moveToNext()) {
          result.add(cursor.getString(nameColumnIndex).toLowerCase(Locale.US));
        }
      }
      return result;
    } finally {
      tryCloseCursor(cursor);
    }
  }

  /**
   * Lists the names of all the columns in the accounts table.
   */
  private Collection<String> listTableColumnNamesLowerCase() {
    return listTableColumnNamesLowerCase(mDatabase, TABLE_NAME);
  }

  /*
   * deleteAllData() will remove all rows. Useful for testing.
   */
  public boolean deleteAllData() {
    mDatabase.delete(AccountDb.TABLE_NAME, null, null);
    return true;
  }

  public boolean nameExists(String email) {
    Cursor cursor = getAccount(email);
    try {
      return !cursorIsEmpty(cursor);
    } finally {
      tryCloseCursor(cursor);
    }
  }

  public String getSecret(String email) {
    Cursor cursor = getAccount(email);
    try {
      if (!cursorIsEmpty(cursor)) {
        cursor.moveToFirst();
        return cursor.getString(cursor.getColumnIndex(SECRET_COLUMN));
      }
    } finally {
      tryCloseCursor(cursor);
    }
    return null;
  }

  static Signer getSigningOracle(String secret) {
    try {
      byte[] keyBytes = decodeKey(secret);
      final Mac mac = Mac.getInstance("HMACSHA1");
      mac.init(new SecretKeySpec(keyBytes, ""));

      // Create a signer object out of the standard Java MAC implementation.
      return new Signer() {
        @Override
        public byte[] sign(byte[] data) {
          return mac.doFinal(data);
        }
      };
    } catch (DecodingException error) {
      Log.e(LOCAL_TAG, error.getMessage());
    } catch (NoSuchAlgorithmException error) {
      Log.e(LOCAL_TAG, error.getMessage());
    } catch (InvalidKeyException error) {
      Log.e(LOCAL_TAG, error.getMessage());
    }

    return null;
  }

  private static byte[] decodeKey(String secret) throws DecodingException {
    return Base32String.decode(secret);
  }

  public Integer getCounter(String email) {
    Cursor cursor = getAccount(email);
    try {
      if (!cursorIsEmpty(cursor)) {
        cursor.moveToFirst();
        return cursor.getInt(cursor.getColumnIndex(COUNTER_COLUMN));
      }
    } finally {
      tryCloseCursor(cursor);
    }
    return null;
  }

  void incrementCounter(String email) {
    ContentValues values = new ContentValues();
    values.put(EMAIL_COLUMN, email);
    Integer counter = getCounter(email);
    values.put(COUNTER_COLUMN, counter + 1);
    mDatabase.update(TABLE_NAME, values, whereClause(email), null);
  }

  public OtpType getType(String email) {
    Cursor cursor = getAccount(email);
    try {
      if (!cursorIsEmpty(cursor)) {
        cursor.moveToFirst();
        Integer value = cursor.getInt(cursor.getColumnIndex(TYPE_COLUMN));
        return OtpType.getEnum(value);
      }
    } finally {
      tryCloseCursor(cursor);
    }
    return null;
  }

  void setType(String email, OtpType type) {
    ContentValues values = new ContentValues();
    values.put(EMAIL_COLUMN, email);
    values.put(TYPE_COLUMN, type.value);
    mDatabase.update(TABLE_NAME, values, whereClause(email), null);
  }

  public boolean isGoogleAccount(String email) {
    Cursor cursor = getAccount(email);
    try {
      if (!cursorIsEmpty(cursor)) {
        cursor.moveToFirst();
        if (cursor.getInt(cursor.getColumnIndex(PROVIDER_COLUMN)) == PROVIDER_GOOGLE) {
          // The account is marked as source: Google
          return true;
        }
        // The account is from an unknown source. Could be a Google account added by scanning
        // a QR code or by manually entering a key
        String emailLowerCase = email.toLowerCase(Locale.US);
        return(emailLowerCase.endsWith("@gmail.com"))
            || (emailLowerCase.endsWith("@google.com"))
            || (email.equals(GOOGLE_CORP_ACCOUNT_NAME));
      }
    } finally {
      tryCloseCursor(cursor);
    }
    return false;
  }

  /**
   * Finds the Google corp account in this database.
   *
   * @return the name of the account if it is present or {@code null} if the account does not exist.
   */
  public String findGoogleCorpAccount() {
    return nameExists(GOOGLE_CORP_ACCOUNT_NAME) ? GOOGLE_CORP_ACCOUNT_NAME : null;
  }

  private static String whereClause(String email) {
    return EMAIL_COLUMN + " = " + DatabaseUtils.sqlEscapeString(email);
  }

  public void delete(String email) {
    mDatabase.delete(TABLE_NAME, whereClause(email), null);
  }

  /**
   * Save key to database, creating a new user entry if necessary.
   * @param email the user email address. When editing, the new user email.
   * @param secret the secret key.
   * @param oldEmail If editing, the original user email, otherwise null.
   * @param type hotp vs totp
   * @param counter only important for the hotp type
   */
  public void update(String email, String secret, String oldEmail,
      OtpType type, Integer counter) {
    update(email, secret, oldEmail, type, counter, null);
  }

  /**
   * Save key to database, creating a new user entry if necessary.
   * @param email the user email address. When editing, the new user email.
   * @param secret the secret key.
   * @param oldEmail If editing, the original user email, otherwise null.
   * @param type hotp vs totp
   * @param counter only important for the hotp type
   * @param googleAccount whether the key is for a Google account or {@code null} to preserve
   *        the previous value (or use a default if adding a key).
   */
  public void update(String email, String secret, String oldEmail,
      OtpType type, Integer counter, Boolean googleAccount) {
    ContentValues values = new ContentValues();
    values.put(EMAIL_COLUMN, email);
    values.put(SECRET_COLUMN, secret);
    values.put(TYPE_COLUMN, type.ordinal());
    values.put(COUNTER_COLUMN, counter);
    if (googleAccount != null) {
      values.put(
          PROVIDER_COLUMN,
          (googleAccount.booleanValue()) ? PROVIDER_GOOGLE : PROVIDER_UNKNOWN);
    }
    int updated = mDatabase.update(TABLE_NAME, values,
                                  whereClause(oldEmail), null);
    if (updated == 0) {
      mDatabase.insert(TABLE_NAME, null, values);
    }
  }

  private Cursor getNames() {
    return mDatabase.query(TABLE_NAME, null, null, null, null, null, null, null);
  }

  private Cursor getAccount(String email) {
    return mDatabase.query(TABLE_NAME, null, EMAIL_COLUMN + "= ?",
        new String[] {email}, null, null, null);
  }

  /**
   * Returns true if the cursor is null, or contains no rows.
   */
  private static boolean cursorIsEmpty(Cursor c) {
    return c == null || c.getCount() == 0;
  }

  /**
   * Closes the cursor if it is not null and not closed.
   */
  private static void tryCloseCursor(Cursor c) {
    if (c != null && !c.isClosed()) {
      c.close();
    }
  }

  /**
   * Get list of all account names.
   * @param result Collection of strings-- account names are appended, without
   *               clearing this collection on entry.
   * @return Number of accounts added to the output parameter.
   */
  public int getNames(Collection<String> result) {
    Cursor cursor = getNames();

    try {
      if (cursorIsEmpty(cursor))
        return 0;

      int nameCount = cursor.getCount();
      int index = cursor.getColumnIndex(AccountDb.EMAIL_COLUMN);

      for (int i = 0; i < nameCount; ++i) {
        cursor.moveToPosition(i);
        String username = cursor.getString(index);
        result.add(username);
      }

      return nameCount;
    } finally {
      tryCloseCursor(cursor);
    }
  }

  private static class AccountDbOpenException extends RuntimeException {
    public AccountDbOpenException(String message, Exception e) {
      super(message, e);
    }
  }

}




Java Source Code List

com.google.android.apps.authenticator.AccountDb.java
com.google.android.apps.authenticator.AddOtherAccountActivity.java
com.google.android.apps.authenticator.AuthenticatorActivity.java
com.google.android.apps.authenticator.AuthenticatorApplication.java
com.google.android.apps.authenticator.Base32String.java
com.google.android.apps.authenticator.CheckCodeActivity.java
com.google.android.apps.authenticator.CountdownIndicator.java
com.google.android.apps.authenticator.EnterKeyActivity.java
com.google.android.apps.authenticator.FileUtilities.java
com.google.android.apps.authenticator.HexEncoding.java
com.google.android.apps.authenticator.MarketBuildOptionalFeatures.java
com.google.android.apps.authenticator.OptionalFeatures.java
com.google.android.apps.authenticator.OtpGenerationNotPermittedException.java
com.google.android.apps.authenticator.OtpProvider.java
com.google.android.apps.authenticator.OtpSourceException.java
com.google.android.apps.authenticator.OtpSource.java
com.google.android.apps.authenticator.PasscodeGenerator.java
com.google.android.apps.authenticator.Preconditions.java
com.google.android.apps.authenticator.RunOnThisLooperThreadExecutor.java
com.google.android.apps.authenticator.SettingsAboutActivity.java
com.google.android.apps.authenticator.SettingsActivity.java
com.google.android.apps.authenticator.TotpClock.java
com.google.android.apps.authenticator.TotpCountdownTask.java
com.google.android.apps.authenticator.TotpCounter.java
com.google.android.apps.authenticator.UserRowView.java
com.google.android.apps.authenticator.Utilities.java
com.google.android.apps.authenticator.dataimport.ExportServiceBasedImportController.java
com.google.android.apps.authenticator.dataimport.ImportController.java
com.google.android.apps.authenticator.dataimport.Importer.java
com.google.android.apps.authenticator.howitworks.IntroEnterCodeActivity.java
com.google.android.apps.authenticator.howitworks.IntroEnterPasswordActivity.java
com.google.android.apps.authenticator.howitworks.IntroVerifyDeviceActivity.java
com.google.android.apps.authenticator.testability.DependencyInjector.java
com.google.android.apps.authenticator.testability.HttpClientFactory.java
com.google.android.apps.authenticator.testability.SharedPreferencesRenamingDelegatingContext.java
com.google.android.apps.authenticator.testability.StartActivityListener.java
com.google.android.apps.authenticator.testability.TestableActivity.java
com.google.android.apps.authenticator.testability.TestablePreferenceActivity.java
com.google.android.apps.authenticator.timesync.AboutActivity.java
com.google.android.apps.authenticator.timesync.NetworkTimeProvider.java
com.google.android.apps.authenticator.timesync.SettingsTimeCorrectionActivity.java
com.google.android.apps.authenticator.timesync.SyncNowActivity.java
com.google.android.apps.authenticator.timesync.SyncNowController.java
com.google.android.apps.authenticator.wizard.WizardPageActivity.java