diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java index d958d742..ca15b5cb 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java @@ -25,7 +25,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import com.beemdevelopment.aegis.encoding.Base32; +import com.beemdevelopment.aegis.encoding.Hex; import com.beemdevelopment.aegis.otp.HotpInfo; +import com.beemdevelopment.aegis.otp.MotpInfo; import com.beemdevelopment.aegis.otp.SteamInfo; import com.beemdevelopment.aegis.otp.TotpInfo; import com.beemdevelopment.aegis.otp.YandexInfo; @@ -44,6 +46,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import dagger.hilt.android.testing.HiltAndroidTest; @@ -78,7 +81,8 @@ public class OverallTest extends AegisTest { generateEntry(HotpInfo.class, "John", "GitHub"), generateEntry(TotpInfo.class, "Alice", "Office 365"), 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) { addEntry(entry); @@ -181,6 +185,8 @@ public class OverallTest extends AegisTest { otpType = "Steam"; } else if (entry.getInfo() instanceof YandexInfo) { otpType = "Yandex"; + } else if (entry.getInfo() instanceof MotpInfo) { + otpType = "MOTP"; } else if (entry.getInfo() instanceof TotpInfo) { otpType = "TOTP"; } else { @@ -191,13 +197,23 @@ public class OverallTest extends AegisTest { 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()); if (entry.getInfo() instanceof YandexInfo) { String pin = "123456"; ((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()); diff --git a/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/MOTP.java b/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/MOTP.java new file mode 100644 index 00000000..2778e289 --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/MOTP.java @@ -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); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java index 9a485ba1..e75a9713 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java @@ -6,6 +6,7 @@ import com.beemdevelopment.aegis.GoogleAuthProtos; import com.beemdevelopment.aegis.encoding.Base32; import com.beemdevelopment.aegis.encoding.Base64; import com.beemdevelopment.aegis.encoding.EncodingException; +import com.beemdevelopment.aegis.encoding.Hex; import com.google.protobuf.InvalidProtocolBufferException; import java.io.Serializable; @@ -37,7 +38,7 @@ public class GoogleAuthInfo implements Serializable { public static GoogleAuthInfo parseUri(Uri uri) throws GoogleAuthInfoException { 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)); } @@ -49,7 +50,7 @@ public class GoogleAuthInfo implements Serializable { byte[] secret; try { - secret = parseSecret(encodedSecret); + secret = (scheme.equals(MotpInfo.SCHEME)) ? Hex.decode(encodedSecret) : parseSecret(encodedSecret); } catch (EncodingException e) { throw new GoogleAuthInfoException(uri, "Bad secret", e); } @@ -57,7 +58,7 @@ public class GoogleAuthInfo implements Serializable { OtpInfo info; String issuer = ""; try { - String type = uri.getHost(); + String type = (scheme.equals(MotpInfo.SCHEME)) ? MotpInfo.ID : uri.getHost(); if (type == null) { 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); issuer = info.getType(); break; + case MotpInfo.ID: + info = new MotpInfo(secret); + break; default: throw new GoogleAuthInfoException(uri, String.format("Unsupported OTP type: %s", type)); } @@ -261,30 +265,36 @@ public class GoogleAuthInfo implements Serializable { public Uri getUri() { Uri.Builder builder = new Uri.Builder(); - builder.scheme(SCHEME); - if (_info instanceof TotpInfo) { - if (_info instanceof SteamInfo) { - 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())); + if (_info instanceof MotpInfo) { + builder.scheme(MotpInfo.SCHEME); + builder.appendQueryParameter("secret", Hex.encode(_info.getSecret())); } else { - throw new RuntimeException(String.format("Unsupported OtpInfo type: %s", _info.getClass())); - } + builder.scheme(SCHEME); - builder.appendQueryParameter("digits", Integer.toString(_info.getDigits())); - builder.appendQueryParameter("algorithm", _info.getAlgorithm(false)); - builder.appendQueryParameter("secret", Base32.encode(_info.getSecret())); + if (_info instanceof TotpInfo) { + if (_info instanceof SteamInfo) { + 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("pin", Base32.encode(((YandexInfo) _info).getPin())); + builder.appendQueryParameter("digits", Integer.toString(_info.getDigits())); + 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("")) { diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/MotpInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/MotpInfo.java new file mode 100644 index 00000000..d08d912f --- /dev/null +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/MotpInfo.java @@ -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()); + } +} diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java index a717c389..85d7973b 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java @@ -70,7 +70,8 @@ public abstract class OtpInfo implements Serializable { } 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 { @@ -118,6 +119,9 @@ public abstract class OtpInfo implements Serializable { case YandexInfo.ID: info = new YandexInfo(secret, obj.getString("pin")); break; + case MotpInfo.ID: + info = new MotpInfo(secret, obj.getString("pin")); + break; default: throw new OtpInfoException("unsupported otp type: " + type); } diff --git a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java index 948299d7..dac4e220 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java @@ -34,6 +34,7 @@ import com.avito.android.krop.KropView; import com.beemdevelopment.aegis.R; import com.beemdevelopment.aegis.encoding.Base32; import com.beemdevelopment.aegis.encoding.EncodingException; +import com.beemdevelopment.aegis.encoding.Hex; import com.beemdevelopment.aegis.helpers.DropdownHelper; import com.beemdevelopment.aegis.helpers.EditTextHelper; 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.otp.GoogleAuthInfo; import com.beemdevelopment.aegis.otp.HotpInfo; +import com.beemdevelopment.aegis.otp.MotpInfo; import com.beemdevelopment.aegis.otp.OtpInfo; import com.beemdevelopment.aegis.otp.OtpInfoException; import com.beemdevelopment.aegis.otp.SteamInfo; @@ -102,8 +104,8 @@ public class EditEntryActivity extends AegisActivity { private TextInputEditText _textDigits; private TextInputLayout _textDigitsLayout; private TextInputEditText _textSecret; - private TextInputEditText _textYandexPin; - private LinearLayout _textYandexPinLayout; + private TextInputEditText _textPin; + private LinearLayout _textPinLayout; private TextInputEditText _textUsageCount; private TextInputEditText _textNote; @@ -156,8 +158,8 @@ public class EditEntryActivity extends AegisActivity { _textDigits = findViewById(R.id.text_digits); _textDigitsLayout = findViewById(R.id.text_digits_layout); _textSecret = findViewById(R.id.text_secret); - _textYandexPin = findViewById(R.id.text_yandex_pin); - _textYandexPinLayout = findViewById(R.id.layout_yandex_pin); + _textPin = findViewById(R.id.text_pin); + _textPinLayout = findViewById(R.id.layout_pin); _textUsageCount = findViewById(R.id.text_usage_count); _textNote = findViewById(R.id.text_note); _dropdownType = findViewById(R.id.dropdown_type); @@ -178,9 +180,9 @@ public class EditEntryActivity extends AegisActivity { layoutBasic.removeView(layoutSecret); if (!_isNew) { secretIndex = 1; - layoutBasic.removeView(_textYandexPinLayout); - layoutAdvanced.addView(_textYandexPinLayout, 0); - ((LinearLayout.LayoutParams) _textYandexPinLayout.getLayoutParams()).topMargin = 0; + layoutBasic.removeView(_textPinLayout); + layoutAdvanced.addView(_textPinLayout, 0); + ((LinearLayout.LayoutParams) _textPinLayout.getLayoutParams()).topMargin = 0; } else { ((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0; } @@ -233,7 +235,7 @@ public class EditEntryActivity extends AegisActivity { byte[] secretBytes = _origEntry.getInfo().getSecret(); if (secretBytes != null) { - String secretString = Base32.encode(secretBytes); + String secretString = (info instanceof MotpInfo) ? Hex.encode(secretBytes) : Base32.encode(secretBytes); _textSecret.setText(secretString); } @@ -241,7 +243,9 @@ public class EditEntryActivity extends AegisActivity { _dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false); 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()); @@ -280,6 +284,12 @@ public class EditEntryActivity extends AegisActivity { _textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD)); _textDigits.setText(String.valueOf(YandexInfo.DIGITS)); 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: throw new RuntimeException(String.format("Unsupported OTP type: %s", type)); } @@ -317,15 +327,17 @@ public class EditEntryActivity extends AegisActivity { } 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); _textPeriodCounterLayout.setEnabled(enabled); _dropdownAlgoLayout.setEnabled(enabled); } private void updatePinFieldVisibility(String otpType) { - boolean visible = otpType.equals(YandexInfo.ID); - _textYandexPinLayout.setVisibility(visible ? View.VISIBLE : View.GONE); + boolean visible = otpType.equals(YandexInfo.ID) || otpType.equals(MotpInfo.ID); + _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) { @@ -662,11 +674,14 @@ public class EditEntryActivity extends AegisActivity { String algo = _dropdownAlgo.getText().toString(); String lowerCasedType = type.toLowerCase(Locale.ROOT); - if (lowerCasedType.equals(YandexInfo.ID)) { - int pinLength = _textYandexPin.length(); + if (lowerCasedType.equals(YandexInfo.ID) || lowerCasedType.equals(MotpInfo.ID)) { + int pinLength = _textPin.length(); if (pinLength < 4) { 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; @@ -679,12 +694,18 @@ public class EditEntryActivity extends AegisActivity { byte[] secret; try { 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) { throw new ParseException("Secret cannot be empty"); } } 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; @@ -706,7 +727,10 @@ public class EditEntryActivity extends AegisActivity { info = new HotpInfo(secret, algo, digits, counter); break; 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; default: throw new RuntimeException(String.format("Unsupported OTP type: %s", type)); diff --git a/app/src/main/res/layout/activity_edit_entry.xml b/app/src/main/res/layout/activity_edit_entry.xml index 5d214982..0991bf06 100644 --- a/app/src/main/res/layout/activity_edit_entry.xml +++ b/app/src/main/res/layout/activity_edit_entry.xml @@ -186,7 +186,7 @@ HOTP Steam Yandex + MOTP diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c5fc2d4f..73ed0c33 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ Save Issuer PIN (4–16 digits) + PIN (4 digits) Suggested Usage count Warning diff --git a/app/src/test/java/com/beemdevelopment/aegis/crypto/otp/MOTPTest.java b/app/src/test/java/com/beemdevelopment/aegis/crypto/otp/MOTPTest.java new file mode 100644 index 00000000..4dad3f34 --- /dev/null +++ b/app/src/test/java/com/beemdevelopment/aegis/crypto/otp/MOTPTest.java @@ -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))); + } +} diff --git a/app/src/test/java/com/beemdevelopment/aegis/otp/MotpInfoTest.java b/app/src/test/java/com/beemdevelopment/aegis/otp/MotpInfoTest.java new file mode 100644 index 00000000..1390fa8b --- /dev/null +++ b/app/src/test/java/com/beemdevelopment/aegis/otp/MotpInfoTest.java @@ -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)); + } + } +}