Improve manual entry validation

This commit is contained in:
Alexander Bakker 2018-01-01 22:14:11 +01:00
parent 4e6dea24c8
commit 97f3d9e2c6
6 changed files with 173 additions and 74 deletions

View file

@ -16,9 +16,12 @@ import android.widget.EditText;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.Spinner; import android.widget.Spinner;
import me.impy.aegis.crypto.CryptoUtils;
import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.encoding.Base32; import me.impy.aegis.encoding.Base32;
import me.impy.aegis.helpers.AuthHelper;
import me.impy.aegis.helpers.SpinnerHelper; import me.impy.aegis.helpers.SpinnerHelper;
public class EditProfileActivity extends AegisActivity { public class EditProfileActivity extends AegisActivity {
@ -89,7 +92,8 @@ public class EditProfileActivity extends AegisActivity {
byte[] secretBytes = entry.getInfo().getSecret(); byte[] secretBytes = entry.getInfo().getSecret();
if (secretBytes != null) { if (secretBytes != null) {
_textSecret.setText(Base32.encodeOriginal(secretBytes)); char[] secretChars = Base32.encode(secretBytes);
_textSecret.setText(secretChars, 0, secretChars.length);
} }
String type = entry.getInfo().getType(); String type = entry.getInfo().getType();
@ -154,6 +158,11 @@ public class EditProfileActivity extends AegisActivity {
} }
private boolean onSave() { private boolean onSave() {
if (_textSecret.length() == 0) {
onError("Secret is a required field.");
return false;
}
int period; int period;
try { try {
period = Integer.parseInt(_textPeriod.getText().toString()); period = Integer.parseInt(_textPeriod.getText().toString());
@ -176,12 +185,20 @@ public class EditProfileActivity extends AegisActivity {
DatabaseEntry entry = _profile.getEntry(); DatabaseEntry entry = _profile.getEntry();
entry.setName(_textName.getText().toString()); entry.setName(_textName.getText().toString());
KeyInfo info = entry.getInfo(); KeyInfo info = entry.getInfo();
info.setIssuer(_textIssuer.getText().toString());
info.setSecret(Base32.decode(_textSecret.getText().toString())); try {
info.setPeriod(period); char[] secret = AuthHelper.getEditTextChars(_textSecret);
info.setDigits(digits); info.setSecret(secret);
info.setAlgorithm(algo); CryptoUtils.zero(secret);
info.setType(type); info.setIssuer(_textIssuer.getText().toString());
info.setPeriod(period);
info.setDigits(digits);
info.setAlgorithm(algo);
info.setType(type);
} catch (KeyInfoException e) {
onError("The entered info is incorrect: " + e.getMessage());
return false;
}
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra("KeyProfile", _profile); intent.putExtra("KeyProfile", _profile);

View file

@ -3,8 +3,10 @@ package me.impy.aegis.crypto;
import android.net.Uri; import android.net.Uri;
import java.io.Serializable; import java.io.Serializable;
import java.util.Arrays;
import me.impy.aegis.encoding.Base32; import me.impy.aegis.encoding.Base32;
import me.impy.aegis.encoding.Base32Exception;
public class KeyInfo implements Serializable { public class KeyInfo implements Serializable {
private String _type = "totp"; private String _type = "totp";
@ -24,7 +26,7 @@ public class KeyInfo implements Serializable {
builder.appendQueryParameter("digits", Integer.toString(_digits)); builder.appendQueryParameter("digits", Integer.toString(_digits));
builder.appendQueryParameter("period", Integer.toString(_period)); builder.appendQueryParameter("period", Integer.toString(_period));
builder.appendQueryParameter("algorithm", _algorithm); builder.appendQueryParameter("algorithm", _algorithm);
builder.appendQueryParameter("secret", Base32.encodeOriginal(_secret)); builder.appendQueryParameter("secret", new String(Base32.encode(_secret)));
if (_type.equals("hotp")) { if (_type.equals("hotp")) {
builder.appendQueryParameter("counter", Long.toString(_counter)); builder.appendQueryParameter("counter", Long.toString(_counter));
} }
@ -44,30 +46,25 @@ 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 KeyInfoException {
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 KeyInfoException("unsupported protocol");
} }
KeyInfo info = new KeyInfo(); KeyInfo info = new KeyInfo();
info.setType(url.getHost());
// only 'totp' and 'hotp' are supported
info._type = url.getHost();
if (info._type.equals("totp") && info._type.equals("hotp")) {
throw new Exception("unsupported type");
}
// 'secret' is a required parameter // 'secret' is a required parameter
String secret = url.getQueryParameter("secret"); String secret = url.getQueryParameter("secret");
if (secret == null) { if (secret == null) {
throw new Exception("'secret' is not set"); throw new KeyInfoException("'secret' is not set");
} }
info._secret = Base32.decode(secret); info.setSecret(secret.toCharArray());
// provider info used to disambiguate accounts // provider info used to disambiguate accounts
String path = url.getPath(); String path = url.getPath();
String label = path != null ? path.substring(1) : ""; String label = path != null && path.length() > 0 ? path.substring(1) : "";
if (label.contains(":")) { if (label.contains(":")) {
// a label can only contain one colon // a label can only contain one colon
@ -75,40 +72,40 @@ public class KeyInfo implements Serializable {
String[] strings = label.split(":"); String[] strings = label.split(":");
if (strings.length == 2) { if (strings.length == 2) {
info._issuer = strings[0]; info.setIssuer(strings[0]);
info._accountName = strings[1]; info.setAccountName(strings[1]);
} else { } else {
// at this point, just dump the whole thing into the accountName // at this point, just dump the whole thing into the accountName
info._accountName = label; info.setAccountName(label);
} }
} else { } else {
// label only contains the account name // label only contains the account name
// grab the issuer's info from the 'issuer' parameter if it's present // grab the issuer's info from the 'issuer' parameter if it's present
String issuer = url.getQueryParameter("issuer"); String issuer = url.getQueryParameter("issuer");
info._issuer = issuer != null ? issuer : ""; info.setIssuer(issuer != null ? issuer : "");
info._accountName = label; info.setAccountName(label);
} }
// 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 = algorithm; info.setAlgorithm(algorithm);
} }
String period = url.getQueryParameter("period"); String period = url.getQueryParameter("period");
if (period != null) { if (period != null) {
info._period = Integer.parseInt(period); info.setPeriod(Integer.parseInt(period));
} }
String digits = url.getQueryParameter("digits"); String digits = url.getQueryParameter("digits");
if (digits != null) { if (digits != null) {
info._digits = Integer.parseInt(digits); info.setDigits(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.parseLong(counter); info.setCounter(Long.parseLong(counter));
} else if (info._type.equals("hotp")) { } else if (info.getType().equals("hotp")) {
throw new Exception("'counter' was not set which is required for 'hotp'"); throw new KeyInfoException("'counter' was not set which is required for 'hotp'");
} }
return info; return info;
@ -117,56 +114,119 @@ public class KeyInfo implements Serializable {
public String getType() { public String getType() {
return _type; return _type;
} }
public byte[] getSecret() { public byte[] getSecret() {
return _secret; return _secret;
} }
public String getAccountName() { public String getAccountName() {
return _accountName; return _accountName;
} }
public String getIssuer() { public String getIssuer() {
return _issuer; return _issuer;
} }
public String getAlgorithm(boolean java) { public String getAlgorithm(boolean java) {
if (java) { if (java) {
return "Hmac" + _algorithm; return "Hmac" + _algorithm;
} }
return _algorithm; return _algorithm;
} }
public int getDigits() { public int getDigits() {
return _digits; return _digits;
} }
public long getCounter() { public long getCounter() {
return _counter; return _counter;
} }
public int getPeriod() { public int getPeriod() {
return _period; return _period;
} }
public void setType(String type) { public boolean isTypeValid(String type) {
return type.equals("totp") || type.equals("hotp");
}
public void setType(String type) throws KeyInfoException {
type = type.toLowerCase();
if (!isTypeValid(type)) {
throw new KeyInfoException(String.format("unsupported otp type: %s", type));
}
_type = type.toLowerCase(); _type = type.toLowerCase();
} }
public void setSecret(char[] base32) throws KeyInfoException {
byte[] secret;
try {
secret = Base32.decode(base32);
} catch (Base32Exception e) {
throw new KeyInfoException("bad secret", e);
}
setSecret(secret);
}
public void setSecret(byte[] secret) { public void setSecret(byte[] secret) {
_secret = secret; _secret = secret;
} }
public void setAccountName(String accountName) { public void setAccountName(String accountName) {
_accountName = accountName; _accountName = accountName;
} }
public void setIssuer(String issuer) { public void setIssuer(String issuer) {
_issuer = issuer; _issuer = issuer;
} }
public void setAlgorithm(String algorithm) {
public boolean isAlgorithmValid(String algorithm) {
return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512");
}
public void setAlgorithm(String algorithm) throws KeyInfoException {
if (algorithm.startsWith("Hmac")) { if (algorithm.startsWith("Hmac")) {
algorithm = algorithm.substring(4); algorithm = algorithm.substring(4);
} }
_algorithm = algorithm.toUpperCase(); algorithm = algorithm.toUpperCase();
if (!isAlgorithmValid(algorithm)) {
throw new KeyInfoException(String.format("unsupported algorithm: %s", algorithm));
}
_algorithm = algorithm;
} }
public void setDigits(int digits) {
public boolean isDigitsValid(int digits) {
return digits == 6 || digits == 8;
}
public void setDigits(int digits) throws KeyInfoException {
if (!isDigitsValid(digits)) {
throw new KeyInfoException(String.format("unsupported amount of digits: %d", digits));
}
_digits = digits; _digits = digits;
} }
public void setCounter(long count) {
public boolean isCounterValid(long count) {
return count >= 0;
}
public void setCounter(long count) throws KeyInfoException {
if (!isCounterValid(count)) {
throw new KeyInfoException(String.format("bad count: %d", count));
}
_counter = count; _counter = count;
} }
public void setPeriod(int period) {
public boolean isPeriodValid(int period) {
return period > 0;
}
public void setPeriod(int period) throws KeyInfoException {
if (!isPeriodValid(period)) {
throw new KeyInfoException(String.format("bad period: %d", period));
}
_period = period; _period = period;
} }
} }

View file

@ -0,0 +1,20 @@
package me.impy.aegis.crypto;
public class KeyInfoException extends Exception {
public KeyInfoException(String message) {
super(message);
}
public KeyInfoException(String message, Throwable cause) {
super(message, cause);
}
@Override
public String getMessage() {
Throwable cause = getCause();
if (cause == null) {
return super.getMessage();
}
return String.format("%s (%s)", super.getMessage(), cause.getMessage());
}
}

View file

@ -1,5 +1,7 @@
package me.impy.aegis.encoding; package me.impy.aegis.encoding;
// modified for use in Aegis
/* (PD) 2001 The Bitzi Corporation /* (PD) 2001 The Bitzi Corporation
* Please see http://bitzi.com/publicdomain for more info. * Please see http://bitzi.com/publicdomain for more info.
* *
@ -20,7 +22,9 @@ package me.impy.aegis.encoding;
* limitations under the License. * limitations under the License.
*/ */
import java.io.UnsupportedEncodingException; import java.util.Arrays;
import me.impy.aegis.crypto.CryptoUtils;
/** /**
* Base32 - encodes and decodes RFC3548 Base32 * Base32 - encodes and decodes RFC3548 Base32
@ -45,16 +49,6 @@ public class Base32 {
0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL'
}; };
public static byte[] encode(byte[] data) throws UnsupportedEncodingException {
String lower = encodeOriginal(data).toLowerCase();
return lower.getBytes("US-ASCII");
}
public static byte[] decodeModified(String data) {
return decode(data.replace('8', 'L').replace('9', 'O'));
}
/** /**
* Encodes byte array to Base32 String. * Encodes byte array to Base32 String.
* *
@ -62,10 +56,10 @@ public class Base32 {
* @return Encoded byte array <code>bytes</code> as a String. * @return Encoded byte array <code>bytes</code> as a String.
* *
*/ */
static public String encodeOriginal(final byte[] bytes) { public static char[] encode(final byte[] bytes) {
int i = 0, index = 0, digit = 0; int i = 0, index = 0, digit = 0, j = 0;
int currByte, nextByte; int currByte, nextByte;
StringBuffer base32 = new StringBuffer((bytes.length + 7) * 8 / 5); char[] base32 = new char[(bytes.length + 7) * 8 / 5];
while (i < bytes.length) { while (i < bytes.length) {
currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign
@ -90,10 +84,12 @@ public class Base32 {
if (index == 0) if (index == 0)
i++; i++;
} }
base32.append(base32Chars.charAt(digit)); base32[j++] = base32Chars.charAt(digit);
} }
return base32.toString(); char[] res = Arrays.copyOf(base32, j);
CryptoUtils.zero(base32);
return res;
} }
/** /**
@ -102,24 +98,13 @@ public class Base32 {
* @param base32 * @param base32
* @return Decoded <code>base32</code> String as a raw byte array. * @return Decoded <code>base32</code> String as a raw byte array.
*/ */
static public byte[] decode(final String base32) { public static byte[] decode(final char[] base32) throws Base32Exception {
int i, index, lookup, offset, digit; int i, index, lookup, offset, digit;
byte[] bytes = new byte[base32.length() * 5 / 8]; byte[] bytes = new byte[base32.length * 5 / 8];
for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { for (i = 0, index = 0, offset = 0; i < base32.length; i++) {
lookup = base32.charAt(i) - '0'; lookup = base32[i] - '0';
digit = decodeDigit(lookup);
/* Skip chars outside the lookup table */
if (lookup < 0 || lookup >= base32Lookup.length) {
continue;
}
digit = base32Lookup[lookup];
/* If this digit is not in the table, ignore it */
if (digit == 0xFF) {
continue;
}
if (index <= 3) { if (index <= 3) {
index = (index + 5) % 8; index = (index + 5) % 8;
@ -144,4 +129,20 @@ public class Base32 {
} }
return bytes; return bytes;
} }
private static int decodeDigit(int c) throws Base32Exception {
/* Skip chars outside the lookup table */
if (c < 0 || c >= base32Lookup.length) {
throw new Base32Exception("char not found in base32 lookup table");
}
int digit = base32Lookup[c];
/* If this digit is not in the table, ignore it */
if (digit == 0xFF) {
throw new Base32Exception("char not found in base32 lookup table");
}
return digit;
}
} }

View file

@ -1,8 +1,7 @@
package me.impy.aegis.encoding; package me.impy.aegis.encoding;
/** public class Base32Exception extends Exception {
* Created by alex on 1/1/18. public Base32Exception(String message) {
*/ super(message);
}
public class Base32Exception {
} }

View file

@ -13,6 +13,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import me.impy.aegis.crypto.KeyInfo; import me.impy.aegis.crypto.KeyInfo;
import me.impy.aegis.crypto.KeyInfoException;
import me.impy.aegis.db.DatabaseEntry; import me.impy.aegis.db.DatabaseEntry;
import me.impy.aegis.util.ByteInputStream; import me.impy.aegis.util.ByteInputStream;
@ -40,7 +41,8 @@ public class FreeOTPImporter extends DatabaseImporter {
return "FreeOTP"; return "FreeOTP";
} }
private static List<DatabaseEntry> parse(XmlPullParser parser) throws IOException, XmlPullParserException, JSONException { private static List<DatabaseEntry> parse(XmlPullParser parser)
throws IOException, XmlPullParserException, JSONException, KeyInfoException {
List<Entry> entries = new ArrayList<>(); List<Entry> entries = new ArrayList<>();
parser.require(XmlPullParser.START_TAG, null, "map"); parser.require(XmlPullParser.START_TAG, null, "map");