mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-14 14:02:49 +00:00
Add an option to import a FreeOTP token file
This commit is contained in:
parent
363fc789ef
commit
37e303626f
12 changed files with 271 additions and 51 deletions
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
130
app/src/main/java/me/impy/aegis/ext/FreeOTPImporter.java
Normal file
130
app/src/main/java/me/impy/aegis/ext/FreeOTPImporter.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
app/src/main/java/me/impy/aegis/ext/KeyConverter.java
Normal file
16
app/src/main/java/me/impy/aegis/ext/KeyConverter.java
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue