diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java index ea9a077a..996c7a20 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/AegisTest.java @@ -78,7 +78,11 @@ public abstract class AegisTest { } protected static VaultEntry generateEntry(Class type, String name, String issuer) { - byte[] secret = CryptoUtils.generateRandomBytes(20); + return generateEntry(type, name, issuer, 20); + } + + protected static VaultEntry generateEntry(Class type, String name, String issuer, int secretLength) { + byte[] secret = CryptoUtils.generateRandomBytes(secretLength); OtpInfo info; try { diff --git a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java index 35ceb8e8..f41b4925 100644 --- a/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java +++ b/app/src/androidTest/java/com/beemdevelopment/aegis/OverallTest.java @@ -70,7 +70,7 @@ 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") + generateEntry(YandexInfo.class, "Ivan", "Yandex", 16) ); for (VaultEntry entry : entries) { addEntry(entry); @@ -108,9 +108,9 @@ public class OverallTest extends AegisTest { changeGroupFilter(_groupName); changeGroupFilter(null); - onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick())); - onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click())); + onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick())); onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click())); + onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(4, click())); onView(withId(R.id.action_share_qr)).perform(click()); onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click()); @@ -171,10 +171,10 @@ public class OverallTest extends AegisTest { otpType = "HOTP"; } else if (entry.getInfo() instanceof SteamInfo) { otpType = "Steam"; - } else if (entry.getInfo() instanceof TotpInfo) { - otpType = "TOTP"; } else if (entry.getInfo() instanceof YandexInfo) { otpType = "Yandex"; + } else if (entry.getInfo() instanceof TotpInfo) { + otpType = "TOTP"; } else { throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName())); } @@ -186,6 +186,12 @@ public class OverallTest extends AegisTest { String 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.action_save)).perform(click()); } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/OTP.java b/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/OTP.java index 868074f9..9480f972 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/OTP.java +++ b/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/OTP.java @@ -5,8 +5,8 @@ import androidx.annotation.NonNull; public class OTP { private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY"; - private int _code; - private int _digits; + private final int _code; + private final int _digits; public OTP(int code, int digits) { _code = code; diff --git a/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/YAOTP.java b/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/YAOTP.java index 6689ede0..c8f1f52f 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/YAOTP.java +++ b/app/src/main/java/com/beemdevelopment/aegis/crypto/otp/YAOTP.java @@ -2,12 +2,11 @@ package com.beemdevelopment.aegis.crypto.otp; import androidx.annotation.NonNull; -import com.beemdevelopment.aegis.util.YandexUtils; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -23,48 +22,37 @@ public class YAOTP { _digits = digits; } - public static YAOTP generateOTP(byte[] secret, byte[] pin, int digits, String otpAlgo, long period) + public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period) throws NoSuchAlgorithmException, InvalidKeyException, IOException { long seconds = System.currentTimeMillis() / 1000; return generateOTP(secret, pin, digits, otpAlgo, seconds, period); } - public static YAOTP generateOTP(byte[] secret, byte[] pin, int digits, String otpAlgo, long seconds, long period) + public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long seconds, long period) throws NoSuchAlgorithmException, InvalidKeyException, IOException { + byte[] pinWithHash; + byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8); + try (ByteArrayOutputStream stream = new ByteArrayOutputStream(pinBytes.length + secret.length)) { + stream.write(pinBytes); + stream.write(secret); + pinWithHash = stream.toByteArray(); + } + + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] keyHash = md.digest(pinWithHash); + if (keyHash[0] == 0) { + keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length); + } long counter = (long) Math.floor((double) seconds / period); + byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter); + int offset = periodHash[periodHash.length - 1] & 0xf; + periodHash[offset] &= 0x7f; + long otp = ByteBuffer.wrap(periodHash) + .order(ByteOrder.BIG_ENDIAN) + .getLong(offset); - try (ByteArrayOutputStream pinWithHashStream = - new ByteArrayOutputStream(pin.length + secret.length)) { - - pinWithHashStream.write(pin); - pinWithHashStream.write(secret, 0, YandexUtils.APPROVED_SECRET_LENGTH); - - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] keyHash = md.digest(pinWithHashStream.toByteArray()); - - if (keyHash[0] == 0) { - keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length); - } - - byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter); - int offset = periodHash[periodHash.length - 1] & 0xf; - - periodHash[offset] &= 0x7f; - long otp = ByteBuffer.wrap(periodHash) - .order(ByteOrder.BIG_ENDIAN) - .getLong(offset); - - return new YAOTP(otp, digits); - } - } - - public long getCode() { - return _code; - } - - public int getDigits() { - return _digits; + return new YAOTP(otp, digits); } @NonNull diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java index df30049f..85fe61fa 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java +++ b/app/src/main/java/com/beemdevelopment/aegis/encoding/Base32.java @@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.encoding; import com.google.common.io.BaseEncoding; +import java.nio.charset.StandardCharsets; import java.util.Locale; public class Base32 { @@ -20,4 +21,9 @@ public class Base32 { public static String encode(byte[] data) { return BaseEncoding.base32().omitPadding().encode(data); } + + public static String encode(String s) { + byte[] bytes = s.getBytes(StandardCharsets.UTF_8); + return encode(bytes); + } } diff --git a/app/src/main/java/com/beemdevelopment/aegis/encoding/EncodingException.java b/app/src/main/java/com/beemdevelopment/aegis/encoding/EncodingException.java index ac75a347..f85ec0ec 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/encoding/EncodingException.java +++ b/app/src/main/java/com/beemdevelopment/aegis/encoding/EncodingException.java @@ -6,4 +6,8 @@ public class EncodingException extends IOException { public EncodingException(Throwable cause) { super(cause); } + + public EncodingException(String message) { + super(message); + } } 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 b2207628..9a485ba1 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/GoogleAuthInfo.java @@ -9,6 +9,7 @@ import com.beemdevelopment.aegis.encoding.EncodingException; import com.google.protobuf.InvalidProtocolBufferException; import java.io.Serializable; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -48,7 +49,7 @@ public class GoogleAuthInfo implements Serializable { byte[] secret; try { - secret = uri.getHost().equals(YandexInfo.OTP_SCHEMA_ID) ? parseYandexSecret(encodedSecret) : parseSecret(encodedSecret); + secret = parseSecret(encodedSecret); } catch (EncodingException e) { throw new GoogleAuthInfoException(uri, "Bad secret", e); } @@ -87,9 +88,13 @@ public class GoogleAuthInfo implements Serializable { hotpInfo.setCounter(Long.parseLong(counter)); info = hotpInfo; break; - case YandexInfo.OTP_SCHEMA_ID: - String pinValue = uri.getQueryParameter("pin"); - info = pinValue != null ? new YandexInfo(secret, parseSecret(pinValue)) : new YandexInfo(secret); + case YandexInfo.HOST_ID: + String pin = uri.getQueryParameter("pin"); + if (pin != null) { + pin = new String(parseSecret(pin), StandardCharsets.UTF_8); + } + + info = new YandexInfo(secret, pin); issuer = info.getType(); break; default: @@ -151,19 +156,6 @@ public class GoogleAuthInfo implements Serializable { return Base32.decode(s); } - /** - * When arrives from Yandex site QR code - there will always be 26 symbols (secret only) - * If it arrives from Aegis Export - it can be 42 (if was manually created) - * Just to be sure, let's check secret length (until final RFC comes up) - */ - public static byte[] parseYandexSecret(String s) throws EncodingException { - if (s.length() == YandexInfo.SECRET_LENGTH || s.length() == YandexInfo.SECRET_FULL_LENGTH) { - return parseSecret(s); - } else { - throw new EncodingException(new Throwable("Length differs from expected")); - } - } - public static Export parseExportUri(String s) throws GoogleAuthInfoException { Uri uri = Uri.parse(s); if (uri == null) { @@ -275,7 +267,7 @@ public class GoogleAuthInfo implements Serializable { if (_info instanceof SteamInfo) { builder.authority("steam"); } else if (_info instanceof YandexInfo) { - builder.authority(YandexInfo.OTP_SCHEMA_ID); + builder.authority(YandexInfo.HOST_ID); } else { builder.authority("totp"); } @@ -292,7 +284,7 @@ public class GoogleAuthInfo implements Serializable { builder.appendQueryParameter("secret", Base32.encode(_info.getSecret())); if (_info instanceof YandexInfo) { - builder.appendQueryParameter("pin", Base32.encode(((YandexInfo) _info).getPinBytes())); + builder.appendQueryParameter("pin", Base32.encode(((YandexInfo) _info).getPin())); } if (_issuer != null && !_issuer.equals("")) { 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 52f035fa..a717c389 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/OtpInfo.java @@ -40,7 +40,7 @@ public abstract class OtpInfo implements Serializable { JSONObject obj = new JSONObject(); try { - obj.put("secret", new String(Base32.encode(getSecret()))); + obj.put("secret", Base32.encode(getSecret())); obj.put("algo", getAlgorithm(false)); obj.put("digits", getDigits()); } catch (JSONException e) { @@ -116,8 +116,7 @@ public abstract class OtpInfo implements Serializable { info = new HotpInfo(secret, algo, digits, obj.getLong("counter")); break; case YandexInfo.ID: - byte[] pin = Base32.decode(obj.getString("pin")); - info = new YandexInfo(secret, pin); + info = new YandexInfo(secret, obj.getString("pin")); break; default: throw new OtpInfoException("unsupported otp type: " + type); diff --git a/app/src/main/java/com/beemdevelopment/aegis/otp/YandexInfo.java b/app/src/main/java/com/beemdevelopment/aegis/otp/YandexInfo.java index 04ebf107..b5da60de 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/otp/YandexInfo.java +++ b/app/src/main/java/com/beemdevelopment/aegis/otp/YandexInfo.java @@ -1,54 +1,63 @@ package com.beemdevelopment.aegis.otp; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.beemdevelopment.aegis.crypto.otp.YAOTP; -import com.beemdevelopment.aegis.encoding.Base32; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; -import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Locale; +import java.util.Objects; public class YandexInfo extends TotpInfo { public static final String DEFAULT_ALGORITHM = "SHA256"; public static final int DIGITS = 8; - public static final int SECRET_LENGTH = 26; - public static final int SECRET_FULL_LENGTH = 42; + public static final int SECRET_LENGTH = 16; + public static final int SECRET_FULL_LENGTH = 26; public static final String ID = "yandex"; - public static final String OTP_SCHEMA_ID = "yaotp"; + public static final String HOST_ID = "yaotp"; - private byte[] _pin; + @Nullable + private String _pin; - public YandexInfo(byte[] secret) throws OtpInfoException { - super(secret, DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD); + public YandexInfo(@NonNull byte[] secret) throws OtpInfoException { + this(secret, null); } - public YandexInfo(byte[] secret, byte[] pin) throws OtpInfoException { + public YandexInfo(@NonNull byte[] secret, @Nullable String pin) throws OtpInfoException { super(secret, DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD); - this._pin = pin; + setSecret(parseSecret(secret)); + _pin = pin; } @Override public String getOtp() { + if (_pin == null) { + throw new IllegalStateException("PIN must be set before generating an OTP"); + } + try { - YAOTP otp = YAOTP.generateOTP(getSecret(), _pin, getDigits(), getAlgorithm(true), getPeriod()); + YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod()); return otp.toString(); } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) { throw new RuntimeException(e); } } + @Nullable public String getPin() { - return _pin != null ? new String(_pin, StandardCharsets.UTF_8) : ""; + return _pin; } - public byte[] getPinBytes() { - return _pin; + public void setPin(@NonNull String pin) { + _pin = pin; } @Override @@ -66,7 +75,7 @@ public class YandexInfo extends TotpInfo { public JSONObject toJson() { JSONObject result = super.toJson(); try { - result.put("pin", Base32.encode(getPinBytes())); + result.put("pin", getPin()); } catch (JSONException e) { throw new RuntimeException(e); } @@ -75,15 +84,105 @@ public class YandexInfo extends TotpInfo { @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof YandexInfo)) return false; + if (!(o instanceof YandexInfo)) { + return false; + } - YandexInfo that = (YandexInfo) o; - return super.equals(o) && Arrays.equals(_pin, that._pin); + YandexInfo info = (YandexInfo) o; + return super.equals(o) && Objects.equals(getPin(), info.getPin()); } - @Override - public int hashCode() { - return super.hashCode() + Arrays.hashCode(_pin); + public static byte[] parseSecret(byte[] secret) throws OtpInfoException { + validateSecret(secret); + + if (secret.length != SECRET_LENGTH) { + return Arrays.copyOfRange(secret, 0, SECRET_LENGTH); + } + + return secret; + } + + /** + * Java implementation of ChecksumIsValid + * From: https://github.com/norblik/KeeYaOtp/blob/188a1a99f13f82e4ef8df8a1b9b9351ba236e2a1/KeeYaOtp/Core/Secret.cs + * License: GPLv3+ + */ + public static void validateSecret(byte[] secret) throws OtpInfoException { + if (secret.length != SECRET_LENGTH && secret.length != SECRET_FULL_LENGTH) { + throw new OtpInfoException(String.format("Invalid Yandex secret length: %d bytes", secret.length)); + } + + // Secrets originating from a QR code do not have a checksum, so we assume those are valid + if (secret.length == SECRET_LENGTH) { + return; + } + + char originalChecksum = (char) ((secret[secret.length - 2] & 0x0F) << 8 | secret[secret.length - 1] & 0xff); + + char accum = 0; + int accumBits = 0; + + int inputTotalBitsAvailable = secret.length * 8 - 12; + int inputIndex = 0; + int inputBitsAvailable = 8; + + while (inputTotalBitsAvailable > 0) { + int requiredBits = 13 - accumBits; + if (inputTotalBitsAvailable < requiredBits) { + requiredBits = inputTotalBitsAvailable; + } + + while (requiredBits > 0) { + int curInput = (secret[inputIndex] & (1 << inputBitsAvailable) - 1) & 0xff; + int bitsToRead = Math.min(requiredBits, inputBitsAvailable); + + curInput >>= inputBitsAvailable - bitsToRead; + accum = (char) (accum << bitsToRead | curInput); + + inputTotalBitsAvailable -= bitsToRead; + requiredBits -= bitsToRead; + inputBitsAvailable -= bitsToRead; + accumBits += bitsToRead; + + if (inputBitsAvailable == 0) { + inputIndex += 1; + inputBitsAvailable = 8; + } + } + + if (accumBits == 13) { + accum ^= 0b1_1000_1111_0011; + } + accumBits = 16 - getNumberOfLeadingZeros(accum); + } + + if (accum != originalChecksum) { + throw new OtpInfoException("Yandex secret checksum invalid"); + } + } + + private static int getNumberOfLeadingZeros(char value) { + if (value == 0) { + return 16; + } + + int n = 0; + if ((value & 0xFF00) == 0) { + n += 8; + value <<= 8; + } + if ((value & 0xF000) == 0) { + n += 4; + value <<= 4; + } + if ((value & 0xC000) == 0) { + n += 2; + value <<= 2; + } + if ((value & 0x8000) == 0) { + n++; + } + + return n; } } 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 a3b86b92..87e17d82 100644 --- a/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java +++ b/app/src/main/java/com/beemdevelopment/aegis/ui/EditEntryActivity.java @@ -32,7 +32,6 @@ import androidx.documentfile.provider.DocumentFile; import com.amulyakhare.textdrawable.TextDrawable; import com.avito.android.krop.KropView; import com.beemdevelopment.aegis.R; -import com.beemdevelopment.aegis.crypto.CryptoUtils; import com.beemdevelopment.aegis.encoding.Base32; import com.beemdevelopment.aegis.encoding.EncodingException; import com.beemdevelopment.aegis.helpers.DropdownHelper; @@ -55,7 +54,6 @@ import com.beemdevelopment.aegis.ui.tasks.ImportFileTask; import com.beemdevelopment.aegis.ui.views.IconAdapter; import com.beemdevelopment.aegis.util.Cloner; import com.beemdevelopment.aegis.util.IOUtils; -import com.beemdevelopment.aegis.util.YandexUtils; import com.beemdevelopment.aegis.vault.VaultEntry; import com.beemdevelopment.aegis.vault.VaultManager; import com.bumptech.glide.Glide; @@ -666,7 +664,7 @@ public class EditEntryActivity extends AegisActivity { if (lowerCasedType.equals(YandexInfo.ID)) { int pinLength = _textYandexPin.length(); if (pinLength < 4) { - throw new ParseException("PIN is a required field. Min 4 digits."); + throw new ParseException("PIN is a required field. Must have a minimum length of 4 digits."); } } @@ -706,11 +704,8 @@ public class EditEntryActivity extends AegisActivity { } info = new HotpInfo(secret, algo, digits, counter); break; - case YandexInfo.OTP_SCHEMA_ID: case YandexInfo.ID: - YandexUtils.validateSecret(secret); - byte[] pin = CryptoUtils.toBytes(_textYandexPin.getText().toString().toCharArray()); - info = new YandexInfo(secret, pin); + info = new YandexInfo(secret, _textYandexPin.getText().toString()); break; default: throw new RuntimeException(String.format("Unsupported OTP type: %s", type)); diff --git a/app/src/main/java/com/beemdevelopment/aegis/util/YandexUtils.java b/app/src/main/java/com/beemdevelopment/aegis/util/YandexUtils.java deleted file mode 100644 index 208fdf29..00000000 --- a/app/src/main/java/com/beemdevelopment/aegis/util/YandexUtils.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.beemdevelopment.aegis.util; - -import com.beemdevelopment.aegis.otp.OtpInfoException; -import com.beemdevelopment.aegis.otp.YandexInfo; - -public class YandexUtils { - private static final char CHECKSUM_POLY = 0b1_1000_1111_0011; - public static final int APPROVED_SECRET_LENGTH = 16; - - private YandexUtils() { - } - - private static int getNumberOfLeadingZeros(char value) { - if (value == 0) return 16; - - int n = 0; - if ((value & 0xFF00) == 0) { - n += 8; - value <<= 8; - } - if ((value & 0xF000) == 0) { - n += 4; - value <<= 4; - } - if ((value & 0xC000) == 0) { - n += 2; - value <<= 2; - } - if ((value & 0x8000) == 0) { - n++; - } - - return n; - } - - /** - * Java implementation of ChecksumIsValid - * from https://github.com/norblik/KeeYaOtp/blob/dev/KeeYaOtp/Core/Secret.cs - */ - public static void validateSecret(byte[] secret) throws OtpInfoException { - /* - When secret comes from QR code - we can't test it, - cause it's only 16 byte long. - */ - if (secret.length == APPROVED_SECRET_LENGTH) return; - - if (secret.length != YandexInfo.SECRET_LENGTH) - throw new OtpInfoException("Wrong secret size"); - - char originalChecksum = (char) ((secret[secret.length - 2] & 0x0F) << 8 | secret[secret.length - 1] & 0xff); - - char accum = 0; - int accumBits = 0; - - int inputTotalBitsAvailable = secret.length * 8 - 12; - int inputIndex = 0; - int inputBitsAvailable = 8; - - while (inputTotalBitsAvailable > 0) { - int requiredBits = 13 - accumBits; - if (inputTotalBitsAvailable < requiredBits) requiredBits = inputTotalBitsAvailable; - - while (requiredBits > 0) { - int curInput = (secret[inputIndex] & (1 << inputBitsAvailable) - 1) & 0xff; - int bitsToRead = Math.min(requiredBits, inputBitsAvailable); - - curInput >>= inputBitsAvailable - bitsToRead; - accum = (char) (accum << bitsToRead | curInput); - - inputTotalBitsAvailable -= bitsToRead; - requiredBits -= bitsToRead; - inputBitsAvailable -= bitsToRead; - accumBits += bitsToRead; - - if (inputBitsAvailable == 0) { - inputIndex += 1; - inputBitsAvailable = 8; - } - } - - if (accumBits == 13) accum ^= CHECKSUM_POLY; - accumBits = 16 - getNumberOfLeadingZeros(accum); - } - - if (accum != originalChecksum) { - throw new OtpInfoException("Secret is corrupted. Checksum is not valid"); - } - } -} diff --git a/app/src/test/java/com/beemdevelopment/aegis/crypto/otp/YAOTPTest.java b/app/src/test/java/com/beemdevelopment/aegis/crypto/otp/YAOTPTest.java index 7d48528e..c215962a 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/crypto/otp/YAOTPTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/crypto/otp/YAOTPTest.java @@ -2,9 +2,9 @@ package com.beemdevelopment.aegis.crypto.otp; import static org.junit.Assert.assertEquals; -import com.beemdevelopment.aegis.crypto.CryptoUtils; import com.beemdevelopment.aegis.encoding.Base32; -import com.beemdevelopment.aegis.encoding.EncodingException; +import com.beemdevelopment.aegis.otp.OtpInfoException; +import com.beemdevelopment.aegis.otp.YandexInfo; import org.junit.Test; @@ -23,11 +23,13 @@ public class YAOTPTest { }; @Test - public void validateYaOtp() throws InvalidKeyException, NoSuchAlgorithmException, IOException { + public void validateYaOtp() + throws InvalidKeyException, NoSuchAlgorithmException, IOException, OtpInfoException { for (Vector testCase : TEST_CASES) { + byte[] secret = YandexInfo.parseSecret(Base32.decode(testCase.secret)); YAOTP otp = YAOTP.generateOTP( - Base32.decode(testCase.secret.substring(0, 26)), - CryptoUtils.toBytes(testCase.pin.toCharArray()), + secret, + testCase.pin, 8, "HmacSHA256", testCase.timestamp, diff --git a/app/src/test/java/com/beemdevelopment/aegis/util/YandexUtilsTest.java b/app/src/test/java/com/beemdevelopment/aegis/util/YandexUtilsTest.java index aec675b6..27eab00e 100644 --- a/app/src/test/java/com/beemdevelopment/aegis/util/YandexUtilsTest.java +++ b/app/src/test/java/com/beemdevelopment/aegis/util/YandexUtilsTest.java @@ -5,6 +5,7 @@ import static org.junit.Assert.assertThrows; import com.beemdevelopment.aegis.encoding.Base32; import com.beemdevelopment.aegis.encoding.EncodingException; import com.beemdevelopment.aegis.otp.OtpInfoException; +import com.beemdevelopment.aegis.otp.YandexInfo; import org.junit.Test; @@ -19,14 +20,14 @@ public class YandexUtilsTest { @Test(expected = Test.None.class) public void testValidationOk() throws EncodingException, OtpInfoException { - YandexUtils.validateSecret(getBase32Vector(0)); - YandexUtils.validateSecret(getBase32Vector(1)); + YandexInfo.validateSecret(getBase32Vector(0)); + YandexInfo.validateSecret(getBase32Vector(1)); } @Test public void testYandexSecretValidation() { - assertThrows(OtpInfoException.class, () -> YandexUtils.validateSecret(getBase32Vector(2))); - assertThrows(OtpInfoException.class, () -> YandexUtils.validateSecret(getBase32Vector(3))); + assertThrows(OtpInfoException.class, () -> YandexInfo.validateSecret(getBase32Vector(2))); + assertThrows(OtpInfoException.class, () -> YandexInfo.validateSecret(getBase32Vector(3))); } private byte[] getBase32Vector(int vectorIndex) throws EncodingException {