mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-22 14:59:14 +00:00
Merge pull request #948 from orange-elephant/Add-mOTP-support
Add MOTP support
This commit is contained in:
commit
b875baacef
11 changed files with 323 additions and 47 deletions
|
@ -25,7 +25,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import androidx.test.filters.LargeTest;
|
import androidx.test.filters.LargeTest;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.encoding.Base32;
|
import com.beemdevelopment.aegis.encoding.Base32;
|
||||||
|
import com.beemdevelopment.aegis.encoding.Hex;
|
||||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||||
|
import com.beemdevelopment.aegis.otp.MotpInfo;
|
||||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||||
import com.beemdevelopment.aegis.otp.YandexInfo;
|
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||||
|
@ -44,6 +46,7 @@ import org.junit.runner.RunWith;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import dagger.hilt.android.testing.HiltAndroidTest;
|
import dagger.hilt.android.testing.HiltAndroidTest;
|
||||||
|
|
||||||
|
@ -78,7 +81,8 @@ public class OverallTest extends AegisTest {
|
||||||
generateEntry(HotpInfo.class, "John", "GitHub"),
|
generateEntry(HotpInfo.class, "John", "GitHub"),
|
||||||
generateEntry(TotpInfo.class, "Alice", "Office 365"),
|
generateEntry(TotpInfo.class, "Alice", "Office 365"),
|
||||||
generateEntry(SteamInfo.class, "Gaben", "Steam"),
|
generateEntry(SteamInfo.class, "Gaben", "Steam"),
|
||||||
generateEntry(YandexInfo.class, "Ivan", "Yandex", 16)
|
generateEntry(YandexInfo.class, "Ivan", "Yandex", 16),
|
||||||
|
generateEntry(MotpInfo.class, "Jimmy McGill", "PfSense", 16)
|
||||||
);
|
);
|
||||||
for (VaultEntry entry : entries) {
|
for (VaultEntry entry : entries) {
|
||||||
addEntry(entry);
|
addEntry(entry);
|
||||||
|
@ -181,6 +185,8 @@ public class OverallTest extends AegisTest {
|
||||||
otpType = "Steam";
|
otpType = "Steam";
|
||||||
} else if (entry.getInfo() instanceof YandexInfo) {
|
} else if (entry.getInfo() instanceof YandexInfo) {
|
||||||
otpType = "Yandex";
|
otpType = "Yandex";
|
||||||
|
} else if (entry.getInfo() instanceof MotpInfo) {
|
||||||
|
otpType = "MOTP";
|
||||||
} else if (entry.getInfo() instanceof TotpInfo) {
|
} else if (entry.getInfo() instanceof TotpInfo) {
|
||||||
otpType = "TOTP";
|
otpType = "TOTP";
|
||||||
} else {
|
} else {
|
||||||
|
@ -191,13 +197,23 @@ public class OverallTest extends AegisTest {
|
||||||
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
onView(withText(otpType)).inRoot(RootMatchers.isPlatformPopup()).perform(click());
|
||||||
}
|
}
|
||||||
|
|
||||||
String secret = Base32.encode(entry.getInfo().getSecret());
|
String secret;
|
||||||
|
if (Objects.equals(entry.getInfo().getTypeId(), MotpInfo.ID)) {
|
||||||
|
secret = Hex.encode(entry.getInfo().getSecret());
|
||||||
|
} else {
|
||||||
|
secret = Base32.encode(entry.getInfo().getSecret());
|
||||||
|
}
|
||||||
|
|
||||||
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
|
onView(withId(R.id.text_secret)).perform(typeText(secret), closeSoftKeyboard());
|
||||||
|
|
||||||
if (entry.getInfo() instanceof YandexInfo) {
|
if (entry.getInfo() instanceof YandexInfo) {
|
||||||
String pin = "123456";
|
String pin = "123456";
|
||||||
((YandexInfo) entry.getInfo()).setPin(pin);
|
((YandexInfo) entry.getInfo()).setPin(pin);
|
||||||
onView(withId(R.id.text_yandex_pin)).perform(typeText(pin), closeSoftKeyboard());
|
onView(withId(R.id.text_pin)).perform(typeText(pin), closeSoftKeyboard());
|
||||||
|
} else if (entry.getInfo() instanceof MotpInfo) {
|
||||||
|
String pin = "1234";
|
||||||
|
((MotpInfo) entry.getInfo()).setPin(pin);
|
||||||
|
onView(withId(R.id.text_pin)).perform(typeText(pin), closeSoftKeyboard());
|
||||||
}
|
}
|
||||||
|
|
||||||
onView(withId(R.id.action_save)).perform(click());
|
onView(withId(R.id.action_save)).perform(click());
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package com.beemdevelopment.aegis.crypto.otp;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import com.beemdevelopment.aegis.encoding.Hex;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public class MOTP {
|
||||||
|
private final String _code;
|
||||||
|
private final int _digits;
|
||||||
|
|
||||||
|
private MOTP(String code, int digits) {
|
||||||
|
_code = code;
|
||||||
|
_digits = digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin)
|
||||||
|
throws NoSuchAlgorithmException {
|
||||||
|
|
||||||
|
return generateOTP(secret, algo, digits, period, pin, System.currentTimeMillis() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin, long time)
|
||||||
|
throws NoSuchAlgorithmException {
|
||||||
|
|
||||||
|
long timeBasedCounter = time / period;
|
||||||
|
String secretAsString = Hex.encode(secret);
|
||||||
|
String toDigest = timeBasedCounter + secretAsString + pin;
|
||||||
|
String code = getDigest(algo, toDigest.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
return new MOTP(code, digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
@NonNull
|
||||||
|
protected static String getDigest(String algo, byte[] toDigest) throws NoSuchAlgorithmException {
|
||||||
|
MessageDigest md = MessageDigest.getInstance(algo);
|
||||||
|
byte[] digest = md.digest(toDigest);
|
||||||
|
|
||||||
|
return Hex.encode(digest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return _code.substring(0, _digits);
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import com.beemdevelopment.aegis.GoogleAuthProtos;
|
||||||
import com.beemdevelopment.aegis.encoding.Base32;
|
import com.beemdevelopment.aegis.encoding.Base32;
|
||||||
import com.beemdevelopment.aegis.encoding.Base64;
|
import com.beemdevelopment.aegis.encoding.Base64;
|
||||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||||
|
import com.beemdevelopment.aegis.encoding.Hex;
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
@ -37,7 +38,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
|
|
||||||
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException {
|
||||||
String scheme = uri.getScheme();
|
String scheme = uri.getScheme();
|
||||||
if (scheme == null || !scheme.equals(SCHEME)) {
|
if (scheme == null || !(scheme.equals(SCHEME) || scheme.equals(MotpInfo.SCHEME))) {
|
||||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
|
throw new GoogleAuthInfoException(uri, String.format("Unsupported protocol: %s", scheme));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +50,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
|
|
||||||
byte[] secret;
|
byte[] secret;
|
||||||
try {
|
try {
|
||||||
secret = parseSecret(encodedSecret);
|
secret = (scheme.equals(MotpInfo.SCHEME)) ? Hex.decode(encodedSecret) : parseSecret(encodedSecret);
|
||||||
} catch (EncodingException e) {
|
} catch (EncodingException e) {
|
||||||
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +58,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
OtpInfo info;
|
OtpInfo info;
|
||||||
String issuer = "";
|
String issuer = "";
|
||||||
try {
|
try {
|
||||||
String type = uri.getHost();
|
String type = (scheme.equals(MotpInfo.SCHEME)) ? MotpInfo.ID : uri.getHost();
|
||||||
if (type == null) {
|
if (type == null) {
|
||||||
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
|
throw new GoogleAuthInfoException(uri, String.format("Host not present in URI: %s", uri.toString()));
|
||||||
}
|
}
|
||||||
|
@ -97,6 +98,9 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
info = new YandexInfo(secret, pin);
|
info = new YandexInfo(secret, pin);
|
||||||
issuer = info.getType();
|
issuer = info.getType();
|
||||||
break;
|
break;
|
||||||
|
case MotpInfo.ID:
|
||||||
|
info = new MotpInfo(secret);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
|
throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type));
|
||||||
}
|
}
|
||||||
|
@ -261,30 +265,36 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
|
|
||||||
public Uri getUri() {
|
public Uri getUri() {
|
||||||
Uri.Builder builder = new Uri.Builder();
|
Uri.Builder builder = new Uri.Builder();
|
||||||
builder.scheme(SCHEME);
|
|
||||||
|
|
||||||
if (_info instanceof TotpInfo) {
|
if (_info instanceof MotpInfo) {
|
||||||
if (_info instanceof SteamInfo) {
|
builder.scheme(MotpInfo.SCHEME);
|
||||||
builder.authority("steam");
|
builder.appendQueryParameter("secret", Hex.encode(_info.getSecret()));
|
||||||
} else if (_info instanceof YandexInfo) {
|
|
||||||
builder.authority(YandexInfo.HOST_ID);
|
|
||||||
} else {
|
|
||||||
builder.authority("totp");
|
|
||||||
}
|
|
||||||
builder.appendQueryParameter("period", Integer.toString(((TotpInfo) _info).getPeriod()));
|
|
||||||
} else if (_info instanceof HotpInfo) {
|
|
||||||
builder.authority("hotp");
|
|
||||||
builder.appendQueryParameter("counter", Long.toString(((HotpInfo) _info).getCounter()));
|
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
|
builder.scheme(SCHEME);
|
||||||
}
|
|
||||||
|
|
||||||
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
|
if (_info instanceof TotpInfo) {
|
||||||
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
|
if (_info instanceof SteamInfo) {
|
||||||
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
|
builder.authority("steam");
|
||||||
|
} else if (_info instanceof YandexInfo) {
|
||||||
|
builder.authority(YandexInfo.HOST_ID);
|
||||||
|
} else {
|
||||||
|
builder.authority("totp");
|
||||||
|
}
|
||||||
|
builder.appendQueryParameter("period", Integer.toString(((TotpInfo) _info).getPeriod()));
|
||||||
|
} else if (_info instanceof HotpInfo) {
|
||||||
|
builder.authority("hotp");
|
||||||
|
builder.appendQueryParameter("counter", Long.toString(((HotpInfo) _info).getCounter()));
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass()));
|
||||||
|
}
|
||||||
|
|
||||||
if (_info instanceof YandexInfo) {
|
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
|
||||||
builder.appendQueryParameter("pin", Base32.encode(((YandexInfo) _info).getPin()));
|
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
|
||||||
|
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
|
||||||
|
|
||||||
|
if (_info instanceof YandexInfo) {
|
||||||
|
builder.appendQueryParameter("pin", Base32.encode(((YandexInfo) _info).getPin()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_issuer != null && !_issuer.equals("")) {
|
if (_issuer != null && !_issuer.equals("")) {
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package com.beemdevelopment.aegis.otp;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.beemdevelopment.aegis.crypto.otp.MOTP;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class MotpInfo extends TotpInfo {
|
||||||
|
public static final String ID = "motp";
|
||||||
|
public static final String SCHEME = "motp";
|
||||||
|
public static final String ALGORITHM = "MD5";
|
||||||
|
|
||||||
|
public static final int PERIOD = 10;
|
||||||
|
public static final int DIGITS = 6;
|
||||||
|
|
||||||
|
private String _pin;
|
||||||
|
|
||||||
|
public MotpInfo(@NonNull byte[] secret) throws OtpInfoException {
|
||||||
|
this(secret, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MotpInfo(byte[] secret, String pin) throws OtpInfoException {
|
||||||
|
super(secret, ALGORITHM, DIGITS, PERIOD);
|
||||||
|
setPin(pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOtp() {
|
||||||
|
if (_pin == null) {
|
||||||
|
throw new IllegalStateException("PIN must be set before generating an OTP");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MOTP otp = MOTP.generateOTP(getSecret(), getAlgorithm(false), getDigits(), getPeriod(), getPin());
|
||||||
|
return otp.toString();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOtp(long time) {
|
||||||
|
if (_pin == null) {
|
||||||
|
throw new IllegalStateException("PIN must be set before generating an OTP");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MOTP otp = MOTP.generateOTP(getSecret(), getAlgorithm(false), getDigits(), getPeriod(), getPin(), time);
|
||||||
|
return otp.toString();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTypeId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject toJson() {
|
||||||
|
JSONObject result = super.toJson();
|
||||||
|
try {
|
||||||
|
result.put("pin", getPin());
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public String getPin() {
|
||||||
|
return _pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPin(@NonNull String pin) {
|
||||||
|
this._pin = pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (!(o instanceof MotpInfo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MotpInfo info = (MotpInfo) o;
|
||||||
|
return super.equals(o) && Objects.equals(getPin(), info.getPin());
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,7 +70,8 @@ public abstract class OtpInfo implements Serializable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isAlgorithmValid(String algorithm) {
|
public static boolean isAlgorithmValid(String algorithm) {
|
||||||
return algorithm.equals("SHA1") || algorithm.equals("SHA256") || algorithm.equals("SHA512");
|
return algorithm.equals("SHA1") || algorithm.equals("SHA256") ||
|
||||||
|
algorithm.equals("SHA512") || algorithm.equals("MD5");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setAlgorithm(String algorithm) throws OtpInfoException {
|
public void setAlgorithm(String algorithm) throws OtpInfoException {
|
||||||
|
@ -118,6 +119,9 @@ public abstract class OtpInfo implements Serializable {
|
||||||
case YandexInfo.ID:
|
case YandexInfo.ID:
|
||||||
info = new YandexInfo(secret, obj.getString("pin"));
|
info = new YandexInfo(secret, obj.getString("pin"));
|
||||||
break;
|
break;
|
||||||
|
case MotpInfo.ID:
|
||||||
|
info = new MotpInfo(secret, obj.getString("pin"));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new OtpInfoException("unsupported otp type: " + type);
|
throw new OtpInfoException("unsupported otp type: " + type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import com.avito.android.krop.KropView;
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
import com.beemdevelopment.aegis.encoding.Base32;
|
import com.beemdevelopment.aegis.encoding.Base32;
|
||||||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||||
|
import com.beemdevelopment.aegis.encoding.Hex;
|
||||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||||
|
@ -42,6 +43,7 @@ import com.beemdevelopment.aegis.icons.IconPack;
|
||||||
import com.beemdevelopment.aegis.icons.IconType;
|
import com.beemdevelopment.aegis.icons.IconType;
|
||||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||||
|
import com.beemdevelopment.aegis.otp.MotpInfo;
|
||||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||||
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||||
|
@ -102,8 +104,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
private TextInputEditText _textDigits;
|
private TextInputEditText _textDigits;
|
||||||
private TextInputLayout _textDigitsLayout;
|
private TextInputLayout _textDigitsLayout;
|
||||||
private TextInputEditText _textSecret;
|
private TextInputEditText _textSecret;
|
||||||
private TextInputEditText _textYandexPin;
|
private TextInputEditText _textPin;
|
||||||
private LinearLayout _textYandexPinLayout;
|
private LinearLayout _textPinLayout;
|
||||||
private TextInputEditText _textUsageCount;
|
private TextInputEditText _textUsageCount;
|
||||||
private TextInputEditText _textNote;
|
private TextInputEditText _textNote;
|
||||||
|
|
||||||
|
@ -156,8 +158,8 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
_textDigits = findViewById(R.id.text_digits);
|
_textDigits = findViewById(R.id.text_digits);
|
||||||
_textDigitsLayout = findViewById(R.id.text_digits_layout);
|
_textDigitsLayout = findViewById(R.id.text_digits_layout);
|
||||||
_textSecret = findViewById(R.id.text_secret);
|
_textSecret = findViewById(R.id.text_secret);
|
||||||
_textYandexPin = findViewById(R.id.text_yandex_pin);
|
_textPin = findViewById(R.id.text_pin);
|
||||||
_textYandexPinLayout = findViewById(R.id.layout_yandex_pin);
|
_textPinLayout = findViewById(R.id.layout_pin);
|
||||||
_textUsageCount = findViewById(R.id.text_usage_count);
|
_textUsageCount = findViewById(R.id.text_usage_count);
|
||||||
_textNote = findViewById(R.id.text_note);
|
_textNote = findViewById(R.id.text_note);
|
||||||
_dropdownType = findViewById(R.id.dropdown_type);
|
_dropdownType = findViewById(R.id.dropdown_type);
|
||||||
|
@ -178,9 +180,9 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
layoutBasic.removeView(layoutSecret);
|
layoutBasic.removeView(layoutSecret);
|
||||||
if (!_isNew) {
|
if (!_isNew) {
|
||||||
secretIndex = 1;
|
secretIndex = 1;
|
||||||
layoutBasic.removeView(_textYandexPinLayout);
|
layoutBasic.removeView(_textPinLayout);
|
||||||
layoutAdvanced.addView(_textYandexPinLayout, 0);
|
layoutAdvanced.addView(_textPinLayout, 0);
|
||||||
((LinearLayout.LayoutParams) _textYandexPinLayout.getLayoutParams()).topMargin = 0;
|
((LinearLayout.LayoutParams) _textPinLayout.getLayoutParams()).topMargin = 0;
|
||||||
} else {
|
} else {
|
||||||
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
|
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
|
||||||
}
|
}
|
||||||
|
@ -233,7 +235,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
|
|
||||||
byte[] secretBytes = _origEntry.getInfo().getSecret();
|
byte[] secretBytes = _origEntry.getInfo().getSecret();
|
||||||
if (secretBytes != null) {
|
if (secretBytes != null) {
|
||||||
String secretString = Base32.encode(secretBytes);
|
String secretString = (info instanceof MotpInfo) ? Hex.encode(secretBytes) : Base32.encode(secretBytes);
|
||||||
_textSecret.setText(secretString);
|
_textSecret.setText(secretString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,7 +243,9 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
|
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
|
||||||
|
|
||||||
if (info instanceof YandexInfo) {
|
if (info instanceof YandexInfo) {
|
||||||
_textYandexPin.setText(((YandexInfo) info).getPin());
|
_textPin.setText(((YandexInfo) info).getPin());
|
||||||
|
} else if (info instanceof MotpInfo) {
|
||||||
|
_textPin.setText(((MotpInfo) info).getPin());
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
||||||
|
@ -280,6 +284,12 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
|
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
|
||||||
_textDigits.setText(String.valueOf(YandexInfo.DIGITS));
|
_textDigits.setText(String.valueOf(YandexInfo.DIGITS));
|
||||||
break;
|
break;
|
||||||
|
case MotpInfo.ID:
|
||||||
|
_dropdownAlgo.setText(MotpInfo.ALGORITHM, false);
|
||||||
|
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
||||||
|
_textPeriodCounter.setText(String.valueOf(MotpInfo.PERIOD));
|
||||||
|
_textDigits.setText(String.valueOf(MotpInfo.DIGITS));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||||
}
|
}
|
||||||
|
@ -317,15 +327,17 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAdvancedFieldStatus(String otpType) {
|
private void updateAdvancedFieldStatus(String otpType) {
|
||||||
boolean enabled = !otpType.equals(SteamInfo.ID) && !otpType.equals(YandexInfo.ID) && (!_isNew || _isManual);
|
boolean enabled = !otpType.equals(SteamInfo.ID) && !otpType.equals(YandexInfo.ID)
|
||||||
|
&& !otpType.equals(MotpInfo.ID) && (!_isNew || _isManual);
|
||||||
_textDigitsLayout.setEnabled(enabled);
|
_textDigitsLayout.setEnabled(enabled);
|
||||||
_textPeriodCounterLayout.setEnabled(enabled);
|
_textPeriodCounterLayout.setEnabled(enabled);
|
||||||
_dropdownAlgoLayout.setEnabled(enabled);
|
_dropdownAlgoLayout.setEnabled(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePinFieldVisibility(String otpType) {
|
private void updatePinFieldVisibility(String otpType) {
|
||||||
boolean visible = otpType.equals(YandexInfo.ID);
|
boolean visible = otpType.equals(YandexInfo.ID) || otpType.equals(MotpInfo.ID);
|
||||||
_textYandexPinLayout.setVisibility(visible ? View.VISIBLE : View.GONE);
|
_textPinLayout.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
_textPin.setHint(otpType.equals(MotpInfo.ID) ? R.string.motp_pin : R.string.yandex_pin);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setGroup(String groupName) {
|
private void setGroup(String groupName) {
|
||||||
|
@ -662,11 +674,14 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
String algo = _dropdownAlgo.getText().toString();
|
String algo = _dropdownAlgo.getText().toString();
|
||||||
String lowerCasedType = type.toLowerCase(Locale.ROOT);
|
String lowerCasedType = type.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
if (lowerCasedType.equals(YandexInfo.ID)) {
|
if (lowerCasedType.equals(YandexInfo.ID) || lowerCasedType.equals(MotpInfo.ID)) {
|
||||||
int pinLength = _textYandexPin.length();
|
int pinLength = _textPin.length();
|
||||||
if (pinLength < 4) {
|
if (pinLength < 4) {
|
||||||
throw new ParseException("PIN is a required field. Must have a minimum length of 4 digits.");
|
throw new ParseException("PIN is a required field. Must have a minimum length of 4 digits.");
|
||||||
}
|
}
|
||||||
|
if (pinLength != 4 && lowerCasedType.equals(MotpInfo.ID)) {
|
||||||
|
throw new ParseException("PIN must have a length of 4 digits.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int digits;
|
int digits;
|
||||||
|
@ -679,12 +694,18 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
byte[] secret;
|
byte[] secret;
|
||||||
try {
|
try {
|
||||||
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
|
String secretString = new String(EditTextHelper.getEditTextChars(_textSecret));
|
||||||
secret = GoogleAuthInfo.parseSecret(secretString);
|
|
||||||
|
secret = (lowerCasedType.equals(MotpInfo.ID)) ?
|
||||||
|
Hex.decode(secretString) : GoogleAuthInfo.parseSecret(secretString);
|
||||||
|
|
||||||
if (secret.length == 0) {
|
if (secret.length == 0) {
|
||||||
throw new ParseException("Secret cannot be empty");
|
throw new ParseException("Secret cannot be empty");
|
||||||
}
|
}
|
||||||
} catch (EncodingException e) {
|
} catch (EncodingException e) {
|
||||||
throw new ParseException("Secret is not valid base32.");
|
String exceptionMessage = (lowerCasedType.equals(MotpInfo.ID)) ?
|
||||||
|
"Secret is not valid hexadecimal" : "Secret is not valid base32.";
|
||||||
|
|
||||||
|
throw new ParseException(exceptionMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
OtpInfo info;
|
OtpInfo info;
|
||||||
|
@ -706,7 +727,10 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
info = new HotpInfo(secret, algo, digits, counter);
|
info = new HotpInfo(secret, algo, digits, counter);
|
||||||
break;
|
break;
|
||||||
case YandexInfo.ID:
|
case YandexInfo.ID:
|
||||||
info = new YandexInfo(secret, _textYandexPin.getText().toString());
|
info = new YandexInfo(secret, _textPin.getText().toString());
|
||||||
|
break;
|
||||||
|
case MotpInfo.ID:
|
||||||
|
info = new MotpInfo(secret, _textPin.getText().toString());
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||||
|
|
|
@ -186,7 +186,7 @@
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/layout_yandex_pin"
|
android:id="@+id/layout_pin"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -205,13 +205,12 @@
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/yandex_pin"
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
app:passwordToggleTint="#949494"
|
app:passwordToggleTint="#949494"
|
||||||
app:passwordToggleEnabled="true">
|
app:passwordToggleEnabled="true">
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputEditText
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
android:id="@+id/text_yandex_pin"
|
android:id="@+id/text_pin"
|
||||||
android:maxLength="16"
|
android:maxLength="16"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
<item>HOTP</item>
|
<item>HOTP</item>
|
||||||
<item>Steam</item>
|
<item>Steam</item>
|
||||||
<item>Yandex</item>
|
<item>Yandex</item>
|
||||||
|
<item>MOTP</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="otp_algo_array">
|
<string-array name="otp_algo_array">
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<string name="save">Save</string>
|
<string name="save">Save</string>
|
||||||
<string name="issuer">Issuer</string>
|
<string name="issuer">Issuer</string>
|
||||||
<string name="yandex_pin">PIN (4–16 digits)</string>
|
<string name="yandex_pin">PIN (4–16 digits)</string>
|
||||||
|
<string name="motp_pin">PIN (4 digits)</string>
|
||||||
<string name="suggested">Suggested</string>
|
<string name="suggested">Suggested</string>
|
||||||
<string name="usage_count">Usage count</string>
|
<string name="usage_count">Usage count</string>
|
||||||
<string name="warning">Warning</string>
|
<string name="warning">Warning</string>
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.beemdevelopment.aegis.crypto.otp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||||
|
import com.beemdevelopment.aegis.encoding.Hex;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public class MOTPTest {
|
||||||
|
public static class Vector {
|
||||||
|
public String Secret;
|
||||||
|
public String OTP;
|
||||||
|
public String Pin;
|
||||||
|
public long Time;
|
||||||
|
|
||||||
|
public Vector(long time, String otp, String pin, String secret) {
|
||||||
|
Time = time;
|
||||||
|
OTP = otp;
|
||||||
|
Pin = pin;
|
||||||
|
Secret = secret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Vector[] VECTORS = {
|
||||||
|
new Vector(165892298, "e7d8b6", "1234", "e3152afee62599c8"),
|
||||||
|
new Vector(123456789, "4ebfb2", "1234", "e3152afee62599c8"),
|
||||||
|
new Vector(165954002 * 10, "ced7b1", "9999", "bbb1912bb5c515be"),
|
||||||
|
new Vector(165954002 * 10 + 2, "ced7b1", "9999", "bbb1912bb5c515be"),
|
||||||
|
new Vector(165953987 * 10, "1a14f8", "9999", "bbb1912bb5c515be"),
|
||||||
|
//should round down
|
||||||
|
new Vector(165953987 * 10 + 8, "1a14f8", "9999", "bbb1912bb5c515be")
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOutputCode() throws NoSuchAlgorithmException, EncodingException {
|
||||||
|
for (Vector vector : VECTORS) {
|
||||||
|
MOTP otp = MOTP.generateOTP(Hex.decode(vector.Secret), "MD5", 6, 10, vector.Pin, vector.Time);
|
||||||
|
assertEquals(vector.OTP, otp.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testGetDigest() throws NoSuchAlgorithmException {
|
||||||
|
assertEquals("355938cfe3b73a624297591972d27c01",
|
||||||
|
MOTP.getDigest("MD5", "BOB".getBytes(StandardCharsets.UTF_8)));
|
||||||
|
assertEquals("16d7a4fca7442dda3ad93c9a726597e4",
|
||||||
|
MOTP.getDigest("MD5", "test1234".getBytes(StandardCharsets.UTF_8)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.beemdevelopment.aegis.otp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import com.beemdevelopment.aegis.crypto.otp.MOTPTest;
|
||||||
|
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||||
|
import com.beemdevelopment.aegis.encoding.Hex;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
public class MotpInfoTest {
|
||||||
|
@Test
|
||||||
|
public void testMotpInfoOtp() throws OtpInfoException, EncodingException {
|
||||||
|
for (MOTPTest.Vector vector : MOTPTest.VECTORS) {
|
||||||
|
MotpInfo info = new MotpInfo(Hex.decode(vector.Secret), vector.Pin);
|
||||||
|
assertEquals(vector.OTP, info.getOtp(vector.Time));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue