Add an option to import a FreeOTP token file

This commit is contained in:
Alexander Bakker 2017-08-26 15:47:57 +02:00
parent 363fc789ef
commit 37e303626f
12 changed files with 271 additions and 51 deletions

View file

@ -3,8 +3,8 @@
package="me.impy.aegis"> package="me.impy.aegis">
<uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT" /> <uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application <application
android:allowBackup="true" android:allowBackup="true"

View file

@ -16,6 +16,7 @@ import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable; import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator; import com.amulyakhare.textdrawable.util.ColorGenerator;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@ -149,7 +150,7 @@ public class KeyProfileAdapter extends RecyclerView.Adapter<KeyProfileAdapter.Ke
try { try {
otp = OTP.generateOTP(this.keyProfile.Info); otp = OTP.generateOTP(this.keyProfile.Info);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); throw new UndeclaredThrowableException(e);
} }
this.keyProfile.Code = otp; this.keyProfile.Code = otp;

View file

@ -26,16 +26,21 @@ import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.Toast; import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.UndeclaredThrowableException; import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.Function;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.crypto.otp.OTP; import me.impy.aegis.crypto.otp.OTP;
import me.impy.aegis.db.DatabaseManager; import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.ext.FreeOTPImporter;
import me.impy.aegis.helpers.SimpleItemTouchHelperCallback; import me.impy.aegis.helpers.SimpleItemTouchHelperCallback;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
@ -43,6 +48,7 @@ public class MainActivity extends AppCompatActivity {
private static final int CODE_ADD_KEYINFO = 1; private static final int CODE_ADD_KEYINFO = 1;
private static final int CODE_DO_INTRO = 2; private static final int CODE_DO_INTRO = 2;
private static final int CODE_DECRYPT = 3; private static final int CODE_DECRYPT = 3;
private static final int CODE_IMPORT = 4;
RecyclerView rvKeyProfiles; RecyclerView rvKeyProfiles;
KeyProfileAdapter mKeyProfileAdapter; KeyProfileAdapter mKeyProfileAdapter;
@ -138,9 +144,41 @@ public class MainActivity extends AppCompatActivity {
case CODE_DECRYPT: case CODE_DECRYPT:
onDecryptResult(resultCode, data); onDecryptResult(resultCode, data);
break; break;
case CODE_IMPORT:
onImportResult(resultCode, data);
break;
} }
} }
private void onImportResult(int resultCode, Intent data) {
InputStream stream = null;
try {
try {
stream = getContentResolver().openInputStream(data.getData());
} catch (Exception e) {
Toast.makeText(this, "An error occurred while trying to open the file", Toast.LENGTH_SHORT).show();
return;
}
FreeOTPImporter importer = new FreeOTPImporter(stream);
try {
for (KeyProfile profile : importer.convert()) {
addKey(profile);
}
} catch (Exception e) {
Toast.makeText(this, "An error occurred while trying to parse the file", Toast.LENGTH_SHORT).show();
return;
}
} finally {
if (stream != null) {
try { stream.close(); }
catch (Exception e) { }
}
}
saveDatabase();
}
private void onGetKeyInfoResult(int resultCode, Intent data) { private void onGetKeyInfoResult(int resultCode, Intent data) {
if (resultCode != RESULT_OK) { if (resultCode != RESULT_OK) {
return; return;
@ -158,29 +196,34 @@ public class MainActivity extends AppCompatActivity {
return; return;
} }
final KeyProfile keyProfile = (KeyProfile) data.getSerializableExtra("KeyProfile"); KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
addKey(profile);
saveDatabase();
}
private void addKey(KeyProfile profile) {
String otp; String otp;
try { try {
otp = OTP.generateOTP(keyProfile.Info); otp = OTP.generateOTP(profile.Info);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
return; return;
} }
keyProfile.Order = mKeyProfiles.size() + 1; profile.Name = profile.Info.getAccountName();
keyProfile.Code = otp; profile.Order = mKeyProfiles.size() + 1;
profile.Code = otp;
try { try {
db.addKey(keyProfile); db.addKey(profile);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
// TODO: feedback // TODO: feedback
return; return;
} }
mKeyProfiles.add(keyProfile); mKeyProfiles.add(profile);
mKeyProfileAdapter.notifyDataSetChanged(); mKeyProfileAdapter.notifyDataSetChanged();
saveDatabase();
} }
private void onDoIntroResult(int resultCode, Intent data) { private void onDoIntroResult(int resultCode, Intent data) {
@ -310,6 +353,11 @@ public class MainActivity extends AppCompatActivity {
Intent preferencesActivity = new Intent(this, PreferencesActivity.class); Intent preferencesActivity = new Intent(this, PreferencesActivity.class);
startActivity(preferencesActivity); startActivity(preferencesActivity);
return true; return true;
case R.id.action_import:
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
startActivityForResult(intent, CODE_IMPORT);
return true;
case R.id.action_lock: case R.id.action_lock:
// TODO: properly close the database // TODO: properly close the database
recreate(); recreate();

View file

@ -58,10 +58,9 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result
try { try {
//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.Info = info; keyProfile.Info = info;
keyProfile.Name = String.format("%s/%s", info.getIssuer(), info.getAccountName());
Intent resultIntent = new Intent(); Intent resultIntent = new Intent();
resultIntent.putExtra("KeyProfile", keyProfile); resultIntent.putExtra("KeyProfile", keyProfile);

View file

@ -16,38 +16,6 @@ public class KeyInfo implements Serializable {
private int digits = 6; private int digits = 6;
private int period = 30; private int period = 30;
public String getType() {
return type;
}
public byte[] getSecret() {
return secret;
}
public String getAccountName() {
return accountName;
}
public String getIssuer() {
return issuer;
}
public String getAlgorithm() {
return "Hmac" + algorithm;
}
public int getDigits() {
return digits;
}
public long getCounter() {
return counter;
}
public int getPeriod() {
return period;
}
private KeyInfo() {
}
public void setCounter(long count) {
counter = count;
}
public String getURL() throws Exception { public String getURL() throws Exception {
Uri.Builder builder = new Uri.Builder(); Uri.Builder builder = new Uri.Builder();
builder.scheme("otpauth"); builder.scheme("otpauth");
@ -75,7 +43,7 @@ public class KeyInfo implements Serializable {
return p - (System.currentTimeMillis() % p); return p - (System.currentTimeMillis() % p);
} }
public static KeyInfo FromURL(String s) throws Exception { public static KeyInfo fromURL(String s) throws Exception {
final Uri url = Uri.parse(s); final Uri url = Uri.parse(s);
if (!url.getScheme().equals("otpauth")) { if (!url.getScheme().equals("otpauth")) {
throw new Exception("unsupported protocol"); throw new Exception("unsupported protocol");
@ -144,4 +112,57 @@ public class KeyInfo implements Serializable {
return info; return info;
} }
public String getType() {
return type;
}
public byte[] getSecret() {
return secret;
}
public String getAccountName() {
return accountName;
}
public String getIssuer() {
return issuer;
}
public String getAlgorithm() {
return "Hmac" + algorithm;
}
public int getDigits() {
return digits;
}
public long getCounter() {
return counter;
}
public int getPeriod() {
return period;
}
public void setType(String type) {
this.type = type.toLowerCase();
}
public void setSecret(byte[] secret) {
this.secret = secret;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public void setIssuer(String issuer) {
this.issuer = issuer;
}
public void setAlgorithm(String algorithm) {
if (algorithm.startsWith("Hmac")) {
algorithm = algorithm.substring(4);
}
this.algorithm = algorithm.toUpperCase();
}
public void setDigits(int digits) {
this.digits = digits;
}
public void setCounter(long count) {
counter = count;
}
public void setPeriod(int period) {
this.period = period;
}
} }

View file

@ -1,12 +1,15 @@
package me.impy.aegis.crypto.otp; package me.impy.aegis.crypto.otp;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.crypto.KeyInfo;
public class OTP { public class OTP {
private OTP() { private OTP() {
} }
public static String generateOTP(KeyInfo info) throws Exception { public static String generateOTP(KeyInfo info) throws InvalidKeyException, NoSuchAlgorithmException {
String otp; String otp;
switch (info.getType()) { switch (info.getType()) {
@ -18,8 +21,7 @@ public class OTP {
otp = HOTP.generateOTP(info.getSecret(), info.getCounter(), info.getDigits(), false, -1); otp = HOTP.generateOTP(info.getSecret(), info.getCounter(), info.getDigits(), false, -1);
break; break;
default: default:
// this should never happen throw new RuntimeException();
throw new Exception("unsupported type");
} }
return otp; return otp;

View file

@ -1,7 +1,6 @@
package me.impy.aegis.crypto.slots; package me.impy.aegis.crypto.slots;
import java.io.Serializable; import java.io.Serializable;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
@ -10,8 +9,6 @@ import java.util.List;
import javax.crypto.BadPaddingException; import javax.crypto.BadPaddingException;
import javax.crypto.Cipher; import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException; import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import me.impy.aegis.crypto.CryptoUtils; import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.util.LittleByteBuffer; import me.impy.aegis.util.LittleByteBuffer;

View file

@ -73,7 +73,7 @@ public class Database {
for (DatabaseEntry e : entries) { for (DatabaseEntry e : entries) {
KeyProfile profile = new KeyProfile(); KeyProfile profile = new KeyProfile();
profile.Name = e.Name; profile.Name = e.Name;
profile.Info = KeyInfo.FromURL(e.URL); profile.Info = KeyInfo.fromURL(e.URL);
profile.Order = e.Order; profile.Order = e.Order;
profile.ID = e.ID; profile.ID = e.ID;
list.add(profile); list.add(profile);

View file

@ -0,0 +1,130 @@
package me.impy.aegis.ext;
import android.util.Xml;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import me.impy.aegis.KeyProfile;
import me.impy.aegis.crypto.KeyInfo;
public class FreeOTPImporter extends KeyConverter {
public FreeOTPImporter(InputStream stream) {
super(stream);
}
private static class Entry {
public String Name;
public String Value;
}
@Override
public List<KeyProfile> convert() throws Exception {
List<KeyProfile> keys = new ArrayList<>();
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(_stream, null);
parser.nextTag();
return parse(parser);
}
private static List<KeyProfile> parse(XmlPullParser parser) throws IOException, XmlPullParserException, JSONException {
List<Entry> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map");
while (parser.next() != XmlPullParser.END_TAG) {
if (parser.getEventType() != XmlPullParser.START_TAG) {
continue;
}
if (!parser.getName().equals("string")) {
skip(parser);
continue;
}
entries.add(parseEntry(parser));
}
List<KeyProfile> profiles = new ArrayList<>();
for (Entry entry : entries) {
if (entry.Name.equals("tokenOrder")) {
// TODO: order
JSONArray array = new JSONArray(entry.Value);
} else {
JSONObject obj = new JSONObject(entry.Value);
KeyInfo key = new KeyInfo();
key.setAlgorithm(obj.getString("algo"));
key.setCounter(obj.getLong("counter"));
key.setDigits(obj.getInt("digits"));
key.setIssuer(obj.getString("issuerExt"));
key.setAccountName(obj.optString("label"));
key.setPeriod(obj.getInt("period"));
key.setType(obj.getString("type"));
byte[] secret = toBytes(obj.getJSONArray("secret"));
key.setSecret(secret);
KeyProfile profile = new KeyProfile();
profile.Info = key;
profiles.add(profile);
}
}
return profiles;
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
bytes[i] = (byte)array.getInt(i);
}
return bytes;
}
private static Entry parseEntry(XmlPullParser parser) throws IOException, XmlPullParserException {
KeyProfile profile = new KeyProfile();
parser.require(XmlPullParser.START_TAG, null, "string");
String name = parser.getAttributeValue(null, "name");
String value = parseText(parser);
parser.require(XmlPullParser.END_TAG, null, "string");
return new Entry() {{ Name = name; Value = value; }};
}
private static String parseText(XmlPullParser parser) throws IOException, XmlPullParserException {
String text = "";
if (parser.next() == XmlPullParser.TEXT) {
text = parser.getText();
parser.nextTag();
}
return text;
}
private static void skip(XmlPullParser parser) throws IOException, XmlPullParserException {
// source: https://developer.android.com/training/basics/network-ops/xml.html
if (parser.getEventType() != XmlPullParser.START_TAG) {
throw new IllegalStateException();
}
int depth = 1;
while (depth != 0) {
switch (parser.next()) {
case XmlPullParser.END_TAG:
depth--;
break;
case XmlPullParser.START_TAG:
depth++;
break;
}
}
}
}

View file

@ -0,0 +1,16 @@
package me.impy.aegis.ext;
import java.io.InputStream;
import java.util.List;
import me.impy.aegis.KeyProfile;
public abstract class KeyConverter {
protected InputStream _stream;
public KeyConverter(InputStream stream) {
_stream = stream;
}
public abstract List<KeyProfile> convert() throws Exception;
}

View file

@ -7,6 +7,11 @@
android:icon="@drawable/ic_lock" android:icon="@drawable/ic_lock"
app:showAsAction="ifRoom" app:showAsAction="ifRoom"
android:title=""/> android:title=""/>
<item
android:id="@+id/action_import"
android:orderInCategory="100"
android:title="@string/action_import"
app:showAsAction="never" />
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:orderInCategory="100" android:orderInCategory="100"

View file

@ -1,6 +1,7 @@
<resources> <resources>
<string name="app_name">Aegis</string> <string name="app_name">Aegis</string>
<string name="action_settings">Settings</string> <string name="action_settings">Settings</string>
<string name="action_import">Import</string>
<string name="title_activity_intro">IntroActivity</string> <string name="title_activity_intro">IntroActivity</string>
<string name="settings">Preferences</string> <string name="settings">Preferences</string>
<string name="pref_night_mode">Night mode</string> <string name="pref_night_mode">Night mode</string>