diff --git a/app/build.gradle b/app/build.gradle index 90c9e36a..e93dcd0f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/main/java/me/impy/aegis/KeyProfile.java b/app/src/main/java/me/impy/aegis/KeyProfile.java index 5ebb9275..bf3d9172 100644 --- a/app/src/main/java/me/impy/aegis/KeyProfile.java +++ b/app/src/main/java/me/impy/aegis/KeyProfile.java @@ -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; } diff --git a/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java b/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java index 5d165c0c..7118db6c 100644 --- a/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java +++ b/app/src/main/java/me/impy/aegis/KeyProfileAdapter.java @@ -74,7 +74,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter 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(); diff --git a/app/src/main/java/me/impy/aegis/ScannerActivity.java b/app/src/main/java/me/impy/aegis/ScannerActivity.java index 1c31cdf5..00818ccc 100644 --- a/app/src/main/java/me/impy/aegis/ScannerActivity.java +++ b/app/src/main/java/me/impy/aegis/ScannerActivity.java @@ -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(); diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java new file mode 100644 index 00000000..6d798253 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java @@ -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); + } +} diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java b/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java index 53366687..945e0a12 100644 --- a/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java +++ b/app/src/main/java/me/impy/aegis/crypto/KeyInfo.java @@ -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'"); } diff --git a/app/src/main/java/me/impy/aegis/db/Database.java b/app/src/main/java/me/impy/aegis/db/Database.java new file mode 100644 index 00000000..fdca08ee --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/Database.java @@ -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 getKeys() throws Exception { + List 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(); + } + } +} diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseHelper.java b/app/src/main/java/me/impy/aegis/db/DatabaseHelper.java new file mode 100644 index 00000000..22f07fcb --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseHelper.java @@ -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); + } +}