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

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

View file

@ -26,16 +26,21 @@ import android.view.View;
import android.widget.LinearLayout;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.UndeclaredThrowableException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import me.impy.aegis.crypto.MasterKey;
import me.impy.aegis.crypto.otp.OTP;
import me.impy.aegis.db.DatabaseManager;
import me.impy.aegis.ext.FreeOTPImporter;
import me.impy.aegis.helpers.SimpleItemTouchHelperCallback;
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_DO_INTRO = 2;
private static final int CODE_DECRYPT = 3;
private static final int CODE_IMPORT = 4;
RecyclerView rvKeyProfiles;
KeyProfileAdapter mKeyProfileAdapter;
@ -138,9 +144,41 @@ public class MainActivity extends AppCompatActivity {
case CODE_DECRYPT:
onDecryptResult(resultCode, data);
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) {
if (resultCode != RESULT_OK) {
return;
@ -158,29 +196,34 @@ public class MainActivity extends AppCompatActivity {
return;
}
final KeyProfile keyProfile = (KeyProfile) data.getSerializableExtra("KeyProfile");
KeyProfile profile = (KeyProfile) data.getSerializableExtra("KeyProfile");
addKey(profile);
saveDatabase();
}
private void addKey(KeyProfile profile) {
String otp;
try {
otp = OTP.generateOTP(keyProfile.Info);
otp = OTP.generateOTP(profile.Info);
} catch (Exception e) {
e.printStackTrace();
return;
}
keyProfile.Order = mKeyProfiles.size() + 1;
keyProfile.Code = otp;
profile.Name = profile.Info.getAccountName();
profile.Order = mKeyProfiles.size() + 1;
profile.Code = otp;
try {
db.addKey(keyProfile);
db.addKey(profile);
} catch (Exception e) {
e.printStackTrace();
// TODO: feedback
return;
}
mKeyProfiles.add(keyProfile);
mKeyProfiles.add(profile);
mKeyProfileAdapter.notifyDataSetChanged();
saveDatabase();
}
private void onDoIntroResult(int resultCode, Intent data) {
@ -310,6 +353,11 @@ public class MainActivity extends AppCompatActivity {
Intent preferencesActivity = new Intent(this, PreferencesActivity.class);
startActivity(preferencesActivity);
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:
// TODO: properly close the database
recreate();

View file

@ -58,10 +58,9 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result
try {
//TODO: Handle non TOTP / HOTP qr codes.
KeyInfo info = KeyInfo.FromURL(rawResult.getText());
KeyInfo info = KeyInfo.fromURL(rawResult.getText());
KeyProfile keyProfile = new KeyProfile();
keyProfile.Info = info;
keyProfile.Name = String.format("%s/%s", info.getIssuer(), info.getAccountName());
Intent resultIntent = new Intent();
resultIntent.putExtra("KeyProfile", keyProfile);

View file

@ -16,38 +16,6 @@ public class KeyInfo implements Serializable {
private int digits = 6;
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 {
Uri.Builder builder = new Uri.Builder();
builder.scheme("otpauth");
@ -75,7 +43,7 @@ public class KeyInfo implements Serializable {
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);
if (!url.getScheme().equals("otpauth")) {
throw new Exception("unsupported protocol");
@ -144,4 +112,57 @@ public class KeyInfo implements Serializable {
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;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import me.impy.aegis.crypto.KeyInfo;
public class OTP {
private OTP() {
}
public static String generateOTP(KeyInfo info) throws Exception {
public static String generateOTP(KeyInfo info) throws InvalidKeyException, NoSuchAlgorithmException {
String otp;
switch (info.getType()) {
@ -18,8 +21,7 @@ public class OTP {
otp = HOTP.generateOTP(info.getSecret(), info.getCounter(), info.getDigits(), false, -1);
break;
default:
// this should never happen
throw new Exception("unsupported type");
throw new RuntimeException();
}
return otp;

View file

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

View file

@ -73,7 +73,7 @@ public class Database {
for (DatabaseEntry e : entries) {
KeyProfile profile = new KeyProfile();
profile.Name = e.Name;
profile.Info = KeyInfo.FromURL(e.URL);
profile.Info = KeyInfo.fromURL(e.URL);
profile.Order = e.Order;
profile.ID = e.ID;
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;
}