mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-20 13:59:14 +00:00
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:
parent
5994be2e4d
commit
8063ba11f1
9 changed files with 210 additions and 15 deletions
|
@ -32,5 +32,6 @@ dependencies {
|
||||||
compile 'com.android.support:support-v4:24.1.1'
|
compile 'com.android.support:support-v4:24.1.1'
|
||||||
compile 'com.android.support:recyclerview-v7:24.1.1'
|
compile 'com.android.support:recyclerview-v7:24.1.1'
|
||||||
compile 'com.yarolegovich:lovely-dialog:1.0.4'
|
compile 'com.yarolegovich:lovely-dialog:1.0.4'
|
||||||
|
compile 'net.zetetic:android-database-sqlcipher:3.5.3@aar'
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,5 +8,6 @@ public class KeyProfile implements Serializable {
|
||||||
public String Name;
|
public String Name;
|
||||||
public String Icon;
|
public String Icon;
|
||||||
public String Code;
|
public String Code;
|
||||||
public KeyInfo KeyInfo;
|
public KeyInfo Info;
|
||||||
|
public int ID;
|
||||||
}
|
}
|
||||||
|
|
|
@ -74,7 +74,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileAdapter.Ke
|
||||||
}
|
}
|
||||||
String otp = "";
|
String otp = "";
|
||||||
try {
|
try {
|
||||||
otp = OTP.generateOTP(this.keyProfile.KeyInfo);
|
otp = OTP.generateOTP(this.keyProfile.Info);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
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)
|
// Return the size of your dataset (invoked by the layout manager)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import android.content.Intent;
|
||||||
import android.support.design.widget.FloatingActionButton;
|
import android.support.design.widget.FloatingActionButton;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.v7.app.AppCompatDelegate;
|
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
|
@ -24,7 +23,9 @@ import java.util.ArrayList;
|
||||||
import github.nisrulz.recyclerviewhelper.RVHItemClickListener;
|
import github.nisrulz.recyclerviewhelper.RVHItemClickListener;
|
||||||
import github.nisrulz.recyclerviewhelper.RVHItemDividerDecoration;
|
import github.nisrulz.recyclerviewhelper.RVHItemDividerDecoration;
|
||||||
import github.nisrulz.recyclerviewhelper.RVHItemTouchHelperCallback;
|
import github.nisrulz.recyclerviewhelper.RVHItemTouchHelperCallback;
|
||||||
|
import me.impy.aegis.crypto.CryptoUtils;
|
||||||
import me.impy.aegis.crypto.OTP;
|
import me.impy.aegis.crypto.OTP;
|
||||||
|
import me.impy.aegis.db.Database;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@ -32,6 +33,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
RecyclerView rvKeyProfiles;
|
RecyclerView rvKeyProfiles;
|
||||||
KeyProfileAdapter mKeyProfileAdapter;
|
KeyProfileAdapter mKeyProfileAdapter;
|
||||||
ArrayList<KeyProfile> mKeyProfiles;
|
ArrayList<KeyProfile> mKeyProfiles;
|
||||||
|
Database database;
|
||||||
|
|
||||||
int count = 0;
|
int count = 0;
|
||||||
|
|
||||||
|
@ -41,7 +43,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
setContentView(R.layout.activity_main);
|
setContentView(R.layout.activity_main);
|
||||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
//AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||||
|
|
||||||
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
||||||
fab.setOnClickListener(new View.OnClickListener() {
|
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<>();
|
mKeyProfiles = new ArrayList<>();
|
||||||
|
|
||||||
rvKeyProfiles = (RecyclerView) findViewById(R.id.rvKeyProfiles);
|
rvKeyProfiles = (RecyclerView) findViewById(R.id.rvKeyProfiles);
|
||||||
|
@ -78,6 +85,15 @@ public class MainActivity extends AppCompatActivity {
|
||||||
helper.attachToRecyclerView(rvKeyProfiles);
|
helper.attachToRecyclerView(rvKeyProfiles);
|
||||||
|
|
||||||
rvKeyProfiles.setAdapter(mKeyProfileAdapter);
|
rvKeyProfiles.setAdapter(mKeyProfileAdapter);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (KeyProfile profile : database.getKeys()) {
|
||||||
|
mKeyProfiles.add(profile);
|
||||||
|
}
|
||||||
|
mKeyProfileAdapter.notifyDataSetChanged();
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -90,7 +106,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
String otp;
|
String otp;
|
||||||
try {
|
try {
|
||||||
otp = OTP.generateOTP(keyProfile.KeyInfo);
|
otp = OTP.generateOTP(keyProfile.Info);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
return;
|
return;
|
||||||
|
@ -117,6 +133,12 @@ public class MainActivity extends AppCompatActivity {
|
||||||
keyProfile.Name = text;
|
keyProfile.Name = text;
|
||||||
mKeyProfiles.add(keyProfile);
|
mKeyProfiles.add(keyProfile);
|
||||||
mKeyProfileAdapter.notifyDataSetChanged();
|
mKeyProfileAdapter.notifyDataSetChanged();
|
||||||
|
|
||||||
|
try {
|
||||||
|
database.addKey(keyProfile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.show();
|
.show();
|
||||||
|
|
|
@ -59,7 +59,7 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result
|
||||||
try {
|
try {
|
||||||
KeyInfo info = KeyInfo.FromURL("otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ");
|
KeyInfo info = KeyInfo.FromURL("otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ");
|
||||||
KeyProfile keyProfile = new KeyProfile();
|
KeyProfile keyProfile = new KeyProfile();
|
||||||
keyProfile.KeyInfo = info;
|
keyProfile.Info = info;
|
||||||
keyProfile.Name = String.format("%s/%s", info.getIssuer(), info.getAccountName());
|
keyProfile.Name = String.format("%s/%s", info.getIssuer(), info.getAccountName());
|
||||||
|
|
||||||
Intent resultIntent = new Intent();
|
Intent resultIntent = new Intent();
|
||||||
|
@ -84,7 +84,7 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result
|
||||||
//TODO: Handle non TOTP / HOTP qr codes.
|
//TODO: Handle non TOTP / HOTP qr codes.
|
||||||
KeyInfo info = KeyInfo.FromURL(rawResult.getText());
|
KeyInfo info = KeyInfo.FromURL(rawResult.getText());
|
||||||
KeyProfile keyProfile = new KeyProfile();
|
KeyProfile keyProfile = new KeyProfile();
|
||||||
keyProfile.KeyInfo = info;
|
keyProfile.Info = info;
|
||||||
keyProfile.Name = String.format("%s/%s", info.getIssuer(), info.getAccountName());
|
keyProfile.Name = String.format("%s/%s", info.getIssuer(), info.getAccountName());
|
||||||
|
|
||||||
Intent resultIntent = new Intent();
|
Intent resultIntent = new Intent();
|
||||||
|
|
16
app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java
Normal file
16
app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@ public class KeyInfo implements Serializable {
|
||||||
private String accountName;
|
private String accountName;
|
||||||
private String issuer;
|
private String issuer;
|
||||||
private long counter;
|
private long counter;
|
||||||
private String algorithm = "HmacSHA1";
|
private String algorithm = "SHA1";
|
||||||
private int digits = 6;
|
private int digits = 6;
|
||||||
private int period = 30;
|
private int period = 30;
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ public class KeyInfo implements Serializable {
|
||||||
return issuer;
|
return issuer;
|
||||||
}
|
}
|
||||||
public String getAlgorithm() {
|
public String getAlgorithm() {
|
||||||
return algorithm;
|
return "Hmac" + algorithm;
|
||||||
}
|
}
|
||||||
public int getDigits() {
|
public int getDigits() {
|
||||||
return digits;
|
return digits;
|
||||||
|
@ -41,7 +41,34 @@ public class KeyInfo implements Serializable {
|
||||||
return period;
|
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() {
|
public long getMillisTillNextRotation() {
|
||||||
long p = period * 1000;
|
long p = period * 1000;
|
||||||
|
@ -96,21 +123,21 @@ public class KeyInfo implements Serializable {
|
||||||
// just use the defaults if these parameters aren't set
|
// just use the defaults if these parameters aren't set
|
||||||
String algorithm = url.getQueryParameter("algorithm");
|
String algorithm = url.getQueryParameter("algorithm");
|
||||||
if (algorithm != null) {
|
if (algorithm != null) {
|
||||||
info.algorithm = "Hmac" + algorithm;
|
info.algorithm = algorithm;
|
||||||
}
|
}
|
||||||
String period = url.getQueryParameter("period");
|
String period = url.getQueryParameter("period");
|
||||||
if (period != null) {
|
if (period != null) {
|
||||||
info.period = Integer.getInteger(period);
|
info.period = Integer.parseInt(period);
|
||||||
}
|
}
|
||||||
String digits = url.getQueryParameter("digits");
|
String digits = url.getQueryParameter("digits");
|
||||||
if (digits != null) {
|
if (digits != null) {
|
||||||
info.digits = Integer.getInteger(digits);
|
info.digits = Integer.parseInt(digits);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'counter' is required if the type is 'hotp'
|
// 'counter' is required if the type is 'hotp'
|
||||||
String counter = url.getQueryParameter("counter");
|
String counter = url.getQueryParameter("counter");
|
||||||
if (counter != null) {
|
if (counter != null) {
|
||||||
info.counter = Long.getLong(counter);
|
info.counter = Long.parseLong(counter);
|
||||||
} else if (info.type.equals("hotp")) {
|
} else if (info.type.equals("hotp")) {
|
||||||
throw new Exception("'counter' was not set which is required for 'hotp'");
|
throw new Exception("'counter' was not set which is required for 'hotp'");
|
||||||
}
|
}
|
||||||
|
|
91
app/src/main/java/me/impy/aegis/db/Database.java
Normal file
91
app/src/main/java/me/impy/aegis/db/Database.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
app/src/main/java/me/impy/aegis/db/DatabaseHelper.java
Normal file
37
app/src/main/java/me/impy/aegis/db/DatabaseHelper.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue