Initial pass on adding support for persistence using sqlcipher

To keep the database schema simple we just save the
otpauth:// url and let KeyInfo take care of checking for
validity.

This patch also includes multiple fixes for the KeyInfo
class.

We still need a separate activity to allow the user to
enter their PIN/password. Currently, "test" is used as the
password for the database.
This commit is contained in:
Impyy 2016-08-22 19:31:03 +02:00
parent 5994be2e4d
commit 8063ba11f1
9 changed files with 210 additions and 15 deletions

View file

@ -32,5 +32,6 @@ dependencies {
compile 'com.android.support:support-v4:24.1.1'
compile 'com.android.support:recyclerview-v7:24.1.1'
compile 'com.yarolegovich:lovely-dialog:1.0.4'
compile 'net.zetetic:android-database-sqlcipher:3.5.3@aar'
testCompile 'junit:junit:4.12'
}

View file

@ -8,5 +8,6 @@ public class KeyProfile implements Serializable {
public String Name;
public String Icon;
public String Code;
public KeyInfo KeyInfo;
public KeyInfo Info;
public int ID;
}

View file

@ -74,7 +74,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileAdapter.Ke
}
String otp = "";
try {
otp = OTP.generateOTP(this.keyProfile.KeyInfo);
otp = OTP.generateOTP(this.keyProfile.Info);
} catch (Exception e) {
e.printStackTrace();
}
@ -136,7 +136,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileAdapter.Ke
}
});
}
}, holder.keyProfile.KeyInfo.getMillisTillNextRotation(), holder.keyProfile.KeyInfo.getPeriod() * 1000);
}, holder.keyProfile.Info.getMillisTillNextRotation(), holder.keyProfile.Info.getPeriod() * 1000);
}
// Return the size of your dataset (invoked by the layout manager)

View file

@ -7,7 +7,6 @@ import android.content.Intent;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
@ -24,7 +23,9 @@ import java.util.ArrayList;
import github.nisrulz.recyclerviewhelper.RVHItemClickListener;
import github.nisrulz.recyclerviewhelper.RVHItemDividerDecoration;
import github.nisrulz.recyclerviewhelper.RVHItemTouchHelperCallback;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.OTP;
import me.impy.aegis.db.Database;
public class MainActivity extends AppCompatActivity {
@ -32,6 +33,7 @@ public class MainActivity extends AppCompatActivity {
RecyclerView rvKeyProfiles;
KeyProfileAdapter mKeyProfileAdapter;
ArrayList<KeyProfile> mKeyProfiles;
Database database;
int count = 0;
@ -41,7 +43,7 @@ public class MainActivity extends AppCompatActivity {
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
//AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@ -52,6 +54,11 @@ public class MainActivity extends AppCompatActivity {
}
});
// demo
char[] password = "test".toCharArray();
database = Database.createInstance(getApplicationContext(), "keys.db", password);
CryptoUtils.zero(password);
mKeyProfiles = new ArrayList<>();
rvKeyProfiles = (RecyclerView) findViewById(R.id.rvKeyProfiles);
@ -78,6 +85,15 @@ public class MainActivity extends AppCompatActivity {
helper.attachToRecyclerView(rvKeyProfiles);
rvKeyProfiles.setAdapter(mKeyProfileAdapter);
try {
for (KeyProfile profile : database.getKeys()) {
mKeyProfiles.add(profile);
}
mKeyProfileAdapter.notifyDataSetChanged();
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
@ -90,7 +106,7 @@ public class MainActivity extends AppCompatActivity {
String otp;
try {
otp = OTP.generateOTP(keyProfile.KeyInfo);
otp = OTP.generateOTP(keyProfile.Info);
} catch (Exception e) {
e.printStackTrace();
return;
@ -117,6 +133,12 @@ public class MainActivity extends AppCompatActivity {
keyProfile.Name = text;
mKeyProfiles.add(keyProfile);
mKeyProfileAdapter.notifyDataSetChanged();
try {
database.addKey(keyProfile);
} catch (Exception e) {
e.printStackTrace();
}
}
})
.show();

View file

@ -59,7 +59,7 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result
try {
KeyInfo info = KeyInfo.FromURL("otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ");
KeyProfile keyProfile = new KeyProfile();
keyProfile.KeyInfo = info;
keyProfile.Info = info;
keyProfile.Name = String.format("%s/%s", info.getIssuer(), info.getAccountName());
Intent resultIntent = new Intent();
@ -84,7 +84,7 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result
//TODO: Handle non TOTP / HOTP qr codes.
KeyInfo info = KeyInfo.FromURL(rawResult.getText());
KeyProfile keyProfile = new KeyProfile();
keyProfile.KeyInfo = info;
keyProfile.Info = info;
keyProfile.Name = String.format("%s/%s", info.getIssuer(), info.getAccountName());
Intent resultIntent = new Intent();

View file

@ -0,0 +1,16 @@
package me.impy.aegis.crypto;
import java.util.Arrays;
public class CryptoUtils {
private CryptoUtils() {
}
public static void zero(char[] data) {
Arrays.fill(data, '\0');
}
public static void zero(byte[] data) {
Arrays.fill(data, (byte)0);
}
}

View file

@ -12,7 +12,7 @@ public class KeyInfo implements Serializable {
private String accountName;
private String issuer;
private long counter;
private String algorithm = "HmacSHA1";
private String algorithm = "SHA1";
private int digits = 6;
private int period = 30;
@ -29,7 +29,7 @@ public class KeyInfo implements Serializable {
return issuer;
}
public String getAlgorithm() {
return algorithm;
return "Hmac" + algorithm;
}
public int getDigits() {
return digits;
@ -41,7 +41,34 @@ public class KeyInfo implements Serializable {
return period;
}
private KeyInfo() { }
private KeyInfo() {
}
public void setCounter(long count) {
counter = count;
}
public String getURL() throws Exception {
Uri.Builder builder = new Uri.Builder();
builder.scheme("otpauth");
builder.authority(type);
builder.appendQueryParameter("period", Integer.toString(period));
builder.appendQueryParameter("algorithm", algorithm);
builder.appendQueryParameter("secret", Base32.encodeOriginal(secret));
if (type.equals("hotp")) {
builder.appendQueryParameter("counter", Long.toString(counter));
}
if (!issuer.equals("")) {
builder.path(String.format("%s:%s", issuer, accountName));
builder.appendQueryParameter("issuer", issuer);
} else {
builder.path(accountName);
}
return builder.build().toString();
}
public long getMillisTillNextRotation() {
long p = period * 1000;
@ -96,21 +123,21 @@ public class KeyInfo implements Serializable {
// just use the defaults if these parameters aren't set
String algorithm = url.getQueryParameter("algorithm");
if (algorithm != null) {
info.algorithm = "Hmac" + algorithm;
info.algorithm = algorithm;
}
String period = url.getQueryParameter("period");
if (period != null) {
info.period = Integer.getInteger(period);
info.period = Integer.parseInt(period);
}
String digits = url.getQueryParameter("digits");
if (digits != null) {
info.digits = Integer.getInteger(digits);
info.digits = Integer.parseInt(digits);
}
// 'counter' is required if the type is 'hotp'
String counter = url.getQueryParameter("counter");
if (counter != null) {
info.counter = Long.getLong(counter);
info.counter = Long.parseLong(counter);
} else if (info.type.equals("hotp")) {
throw new Exception("'counter' was not set which is required for 'hotp'");
}

View file

@ -0,0 +1,91 @@
package me.impy.aegis.db;
import android.content.Context;
import net.sqlcipher.Cursor;
import net.sqlcipher.database.SQLiteDatabase;
import java.util.ArrayList;
import java.util.List;
import me.impy.aegis.KeyProfile;
import me.impy.aegis.crypto.KeyInfo;
public class Database {
private static Database instance;
private static Boolean libsLoaded = false;
private SQLiteDatabase db;
private Database(Context context, String filename, char[] password) {
DatabaseHelper helper = new DatabaseHelper(context, filename);
db = helper.getWritableDatabase(password);
}
public static Database createInstance(Context context, String filename, char[] password) {
// load the sqlcipher library, once
if (!libsLoaded) {
SQLiteDatabase.loadLibs(context);
libsLoaded = true;
}
if (instance == null) {
instance = new Database(context, filename, password);
}
return instance;
}
// adds a key to the database and returns it's ID
public void addKey(KeyProfile profile) throws Exception {
db.execSQL("insert into otp (name, url) values (?, ?)",
new Object[]{ profile.Name, profile.Info.getURL() });
profile.ID = getLastID(db, "otp");
}
public void updateKey(KeyProfile profile) throws Exception {
db.execSQL("update otp set name=? url=? where id=?",
new Object[]{ profile.Name, profile.Info.getURL(), profile.ID });
}
public void removeKey(KeyProfile profile) {
db.execSQL("delete from otp where id=?", new Object[]{ profile.ID });
}
public List<KeyProfile> getKeys() throws Exception {
List<KeyProfile> list = new ArrayList<>();
Cursor cursor = db.rawQuery("select * from otp", null);
try {
while (cursor.moveToNext()) {
KeyProfile profile = new KeyProfile();
profile.ID = cursor.getInt(cursor.getColumnIndexOrThrow("id"));
profile.Name = cursor.getString(cursor.getColumnIndexOrThrow("name"));
String url = cursor.getString(cursor.getColumnIndexOrThrow("url"));
profile.Info = KeyInfo.FromURL(url);
list.add(profile);
}
return list;
} finally {
cursor.close();
}
}
public void close() {
db.close();
}
private int getLastID(SQLiteDatabase db, String table) throws Exception {
Cursor cursor = db.rawQuery(String.format("select id from %s order by id desc limit 1", table), null);
try {
if (!cursor.moveToFirst()) {
throw new Exception("no items in the table, this should not happen here");
}
return cursor.getInt(cursor.getColumnIndexOrThrow("id"));
} finally {
cursor.close();
}
}
}

View file

@ -0,0 +1,37 @@
package me.impy.aegis.db;
import android.content.Context;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteOpenHelper;
import java.io.File;
import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.encoding.Base32;
public class DatabaseHelper extends SQLiteOpenHelper {
// NOTE: increment this every time the schema is changed
public static final int Version = 1;
private static final String queryCreateOTPTable =
"create table otp (" +
"id integer primary key autoincrement, " +
"name varchar not null, " +
"url varchar not null)";
public DatabaseHelper(Context context, String filename) {
super(context, filename, null, Version);
}
public void onCreate(SQLiteDatabase db) {
db.execSQL(queryCreateOTPTable);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//db.execSQL(SQL_DELETE_ENTRIES);
//onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
}