mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-14 22:12:55 +00:00
Refactor Yandex support a bit and fix some style issues
This commit is contained in:
parent
76c89f0fcc
commit
3d13117752
13 changed files with 199 additions and 192 deletions
|
@ -78,7 +78,11 @@ public abstract class AegisTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
|
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer) {
|
||||||
byte[] secret = CryptoUtils.generateRandomBytes(20);
|
return generateEntry(type, name, issuer, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static <T extends OtpInfo> VaultEntry generateEntry(Class<T> type, String name, String issuer, int secretLength) {
|
||||||
|
byte[] secret = CryptoUtils.generateRandomBytes(secretLength);
|
||||||
|
|
||||||
OtpInfo info;
|
OtpInfo info;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -70,7 +70,7 @@ 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")
|
generateEntry(YandexInfo.class, "Ivan", "Yandex", 16)
|
||||||
);
|
);
|
||||||
for (VaultEntry entry : entries) {
|
for (VaultEntry entry : entries) {
|
||||||
addEntry(entry);
|
addEntry(entry);
|
||||||
|
@ -108,9 +108,9 @@ public class OverallTest extends AegisTest {
|
||||||
changeGroupFilter(_groupName);
|
changeGroupFilter(_groupName);
|
||||||
changeGroupFilter(null);
|
changeGroupFilter(null);
|
||||||
|
|
||||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(1, longClick()));
|
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, longClick()));
|
||||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(2, click()));
|
|
||||||
onView(withId(R.id.rvKeyProfiles)).perform(RecyclerViewActions.actionOnItemAtPosition(3, click()));
|
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.action_share_qr)).perform(click());
|
||||||
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
onView(withId(R.id.btnNext)).perform(click()).perform(click()).perform(click());
|
||||||
|
|
||||||
|
@ -171,10 +171,10 @@ public class OverallTest extends AegisTest {
|
||||||
otpType = "HOTP";
|
otpType = "HOTP";
|
||||||
} else if (entry.getInfo() instanceof SteamInfo) {
|
} else if (entry.getInfo() instanceof SteamInfo) {
|
||||||
otpType = "Steam";
|
otpType = "Steam";
|
||||||
} else if (entry.getInfo() instanceof TotpInfo) {
|
|
||||||
otpType = "TOTP";
|
|
||||||
} else if (entry.getInfo() instanceof YandexInfo) {
|
} else if (entry.getInfo() instanceof YandexInfo) {
|
||||||
otpType = "Yandex";
|
otpType = "Yandex";
|
||||||
|
} else if (entry.getInfo() instanceof TotpInfo) {
|
||||||
|
otpType = "TOTP";
|
||||||
} else {
|
} else {
|
||||||
throw new RuntimeException(String.format("Unexpected entry type: %s", entry.getInfo().getClass().getSimpleName()));
|
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());
|
String 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) {
|
||||||
|
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());
|
onView(withId(R.id.action_save)).perform(click());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import androidx.annotation.NonNull;
|
||||||
public class OTP {
|
public class OTP {
|
||||||
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
|
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
|
||||||
|
|
||||||
private int _code;
|
private final int _code;
|
||||||
private int _digits;
|
private final int _digits;
|
||||||
|
|
||||||
public OTP(int code, int digits) {
|
public OTP(int code, int digits) {
|
||||||
_code = code;
|
_code = code;
|
||||||
|
|
|
@ -2,12 +2,11 @@ package com.beemdevelopment.aegis.crypto.otp;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.util.YandexUtils;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
@ -23,33 +22,31 @@ public class YAOTP {
|
||||||
_digits = digits;
|
_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 {
|
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||||
long seconds = System.currentTimeMillis() / 1000;
|
long seconds = System.currentTimeMillis() / 1000;
|
||||||
return generateOTP(secret, pin, digits, otpAlgo, seconds, period);
|
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 {
|
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||||
|
byte[] pinWithHash;
|
||||||
long counter = (long) Math.floor((double) seconds / period);
|
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
|
||||||
|
try (ByteArrayOutputStream stream = new ByteArrayOutputStream(pinBytes.length + secret.length)) {
|
||||||
try (ByteArrayOutputStream pinWithHashStream =
|
stream.write(pinBytes);
|
||||||
new ByteArrayOutputStream(pin.length + secret.length)) {
|
stream.write(secret);
|
||||||
|
pinWithHash = stream.toByteArray();
|
||||||
pinWithHashStream.write(pin);
|
}
|
||||||
pinWithHashStream.write(secret, 0, YandexUtils.APPROVED_SECRET_LENGTH);
|
|
||||||
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
byte[] keyHash = md.digest(pinWithHashStream.toByteArray());
|
byte[] keyHash = md.digest(pinWithHash);
|
||||||
|
|
||||||
if (keyHash[0] == 0) {
|
if (keyHash[0] == 0) {
|
||||||
keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length);
|
keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long counter = (long) Math.floor((double) seconds / period);
|
||||||
byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter);
|
byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter);
|
||||||
int offset = periodHash[periodHash.length - 1] & 0xf;
|
int offset = periodHash[periodHash.length - 1] & 0xf;
|
||||||
|
|
||||||
periodHash[offset] &= 0x7f;
|
periodHash[offset] &= 0x7f;
|
||||||
long otp = ByteBuffer.wrap(periodHash)
|
long otp = ByteBuffer.wrap(periodHash)
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
.order(ByteOrder.BIG_ENDIAN)
|
||||||
|
@ -57,15 +54,6 @@ public class YAOTP {
|
||||||
|
|
||||||
return new YAOTP(otp, digits);
|
return new YAOTP(otp, digits);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public long getCode() {
|
|
||||||
return _code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDigits() {
|
|
||||||
return _digits;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,6 +2,7 @@ package com.beemdevelopment.aegis.encoding;
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding;
|
import com.google.common.io.BaseEncoding;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class Base32 {
|
public class Base32 {
|
||||||
|
@ -20,4 +21,9 @@ public class Base32 {
|
||||||
public static String encode(byte[] data) {
|
public static String encode(byte[] data) {
|
||||||
return BaseEncoding.base32().omitPadding().encode(data);
|
return BaseEncoding.base32().omitPadding().encode(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String encode(String s) {
|
||||||
|
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
|
||||||
|
return encode(bytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,4 +6,8 @@ public class EncodingException extends IOException {
|
||||||
public EncodingException(Throwable cause) {
|
public EncodingException(Throwable cause) {
|
||||||
super(cause);
|
super(cause);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EncodingException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
|
|
||||||
byte[] secret;
|
byte[] secret;
|
||||||
try {
|
try {
|
||||||
secret = uri.getHost().equals(YandexInfo.OTP_SCHEMA_ID) ? parseYandexSecret(encodedSecret) : parseSecret(encodedSecret);
|
secret = parseSecret(encodedSecret);
|
||||||
} catch (EncodingException e) {
|
} catch (EncodingException e) {
|
||||||
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
||||||
}
|
}
|
||||||
|
@ -87,9 +88,13 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
hotpInfo.setCounter(Long.parseLong(counter));
|
hotpInfo.setCounter(Long.parseLong(counter));
|
||||||
info = hotpInfo;
|
info = hotpInfo;
|
||||||
break;
|
break;
|
||||||
case YandexInfo.OTP_SCHEMA_ID:
|
case YandexInfo.HOST_ID:
|
||||||
String pinValue = uri.getQueryParameter("pin");
|
String pin = uri.getQueryParameter("pin");
|
||||||
info = pinValue != null ? new YandexInfo(secret, parseSecret(pinValue)) : new YandexInfo(secret);
|
if (pin != null) {
|
||||||
|
pin = new String(parseSecret(pin), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
info = new YandexInfo(secret, pin);
|
||||||
issuer = info.getType();
|
issuer = info.getType();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -151,19 +156,6 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
return Base32.decode(s);
|
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 {
|
public static Export parseExportUri(String s) throws GoogleAuthInfoException {
|
||||||
Uri uri = Uri.parse(s);
|
Uri uri = Uri.parse(s);
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
|
@ -275,7 +267,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
if (_info instanceof SteamInfo) {
|
if (_info instanceof SteamInfo) {
|
||||||
builder.authority("steam");
|
builder.authority("steam");
|
||||||
} else if (_info instanceof YandexInfo) {
|
} else if (_info instanceof YandexInfo) {
|
||||||
builder.authority(YandexInfo.OTP_SCHEMA_ID);
|
builder.authority(YandexInfo.HOST_ID);
|
||||||
} else {
|
} else {
|
||||||
builder.authority("totp");
|
builder.authority("totp");
|
||||||
}
|
}
|
||||||
|
@ -292,7 +284,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
|
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
|
||||||
|
|
||||||
if (_info instanceof YandexInfo) {
|
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("")) {
|
if (_issuer != null && !_issuer.equals("")) {
|
||||||
|
|
|
@ -40,7 +40,7 @@ public abstract class OtpInfo implements Serializable {
|
||||||
JSONObject obj = new JSONObject();
|
JSONObject obj = new JSONObject();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
obj.put("secret", new String(Base32.encode(getSecret())));
|
obj.put("secret", Base32.encode(getSecret()));
|
||||||
obj.put("algo", getAlgorithm(false));
|
obj.put("algo", getAlgorithm(false));
|
||||||
obj.put("digits", getDigits());
|
obj.put("digits", getDigits());
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
|
@ -116,8 +116,7 @@ public abstract class OtpInfo implements Serializable {
|
||||||
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
|
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
|
||||||
break;
|
break;
|
||||||
case YandexInfo.ID:
|
case YandexInfo.ID:
|
||||||
byte[] pin = Base32.decode(obj.getString("pin"));
|
info = new YandexInfo(secret, obj.getString("pin"));
|
||||||
info = new YandexInfo(secret, pin);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new OtpInfoException("unsupported otp type: " + type);
|
throw new OtpInfoException("unsupported otp type: " + type);
|
||||||
|
|
|
@ -1,54 +1,63 @@
|
||||||
package com.beemdevelopment.aegis.otp;
|
package com.beemdevelopment.aegis.otp;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.crypto.otp.YAOTP;
|
import com.beemdevelopment.aegis.crypto.otp.YAOTP;
|
||||||
import com.beemdevelopment.aegis.encoding.Base32;
|
|
||||||
|
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.InvalidKeyException;
|
import java.security.InvalidKeyException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class YandexInfo extends TotpInfo {
|
public class YandexInfo extends TotpInfo {
|
||||||
public static final String DEFAULT_ALGORITHM = "SHA256";
|
public static final String DEFAULT_ALGORITHM = "SHA256";
|
||||||
public static final int DIGITS = 8;
|
public static final int DIGITS = 8;
|
||||||
|
|
||||||
public static final int SECRET_LENGTH = 26;
|
public static final int SECRET_LENGTH = 16;
|
||||||
public static final int SECRET_FULL_LENGTH = 42;
|
public static final int SECRET_FULL_LENGTH = 26;
|
||||||
public static final String ID = "yandex";
|
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 {
|
public YandexInfo(@NonNull byte[] secret) throws OtpInfoException {
|
||||||
super(secret, DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
|
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);
|
super(secret, DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
|
||||||
this._pin = pin;
|
setSecret(parseSecret(secret));
|
||||||
|
_pin = pin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getOtp() {
|
public String getOtp() {
|
||||||
|
if (_pin == null) {
|
||||||
|
throw new IllegalStateException("PIN must be set before generating an OTP");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
YAOTP otp = YAOTP.generateOTP(getSecret(), _pin, getDigits(), getAlgorithm(true), getPeriod());
|
YAOTP otp = YAOTP.generateOTP(getSecret(), getPin(), getDigits(), getAlgorithm(true), getPeriod());
|
||||||
return otp.toString();
|
return otp.toString();
|
||||||
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
public String getPin() {
|
public String getPin() {
|
||||||
return _pin != null ? new String(_pin, StandardCharsets.UTF_8) : "";
|
return _pin;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getPinBytes() {
|
public void setPin(@NonNull String pin) {
|
||||||
return _pin;
|
_pin = pin;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -66,7 +75,7 @@ public class YandexInfo extends TotpInfo {
|
||||||
public JSONObject toJson() {
|
public JSONObject toJson() {
|
||||||
JSONObject result = super.toJson();
|
JSONObject result = super.toJson();
|
||||||
try {
|
try {
|
||||||
result.put("pin", Base32.encode(getPinBytes()));
|
result.put("pin", getPin());
|
||||||
} catch (JSONException e) {
|
} catch (JSONException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
@ -75,15 +84,105 @@ public class YandexInfo extends TotpInfo {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (!(o instanceof YandexInfo)) {
|
||||||
if (!(o instanceof YandexInfo)) return false;
|
return false;
|
||||||
|
|
||||||
YandexInfo that = (YandexInfo) o;
|
|
||||||
return super.equals(o) && Arrays.equals(_pin, that._pin);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
YandexInfo info = (YandexInfo) o;
|
||||||
public int hashCode() {
|
return super.equals(o) && Objects.equals(getPin(), info.getPin());
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,6 @@ import androidx.documentfile.provider.DocumentFile;
|
||||||
import com.amulyakhare.textdrawable.TextDrawable;
|
import com.amulyakhare.textdrawable.TextDrawable;
|
||||||
import com.avito.android.krop.KropView;
|
import com.avito.android.krop.KropView;
|
||||||
import com.beemdevelopment.aegis.R;
|
import com.beemdevelopment.aegis.R;
|
||||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
|
||||||
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.helpers.DropdownHelper;
|
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.ui.views.IconAdapter;
|
||||||
import com.beemdevelopment.aegis.util.Cloner;
|
import com.beemdevelopment.aegis.util.Cloner;
|
||||||
import com.beemdevelopment.aegis.util.IOUtils;
|
import com.beemdevelopment.aegis.util.IOUtils;
|
||||||
import com.beemdevelopment.aegis.util.YandexUtils;
|
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
@ -666,7 +664,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
if (lowerCasedType.equals(YandexInfo.ID)) {
|
if (lowerCasedType.equals(YandexInfo.ID)) {
|
||||||
int pinLength = _textYandexPin.length();
|
int pinLength = _textYandexPin.length();
|
||||||
if (pinLength < 4) {
|
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);
|
info = new HotpInfo(secret, algo, digits, counter);
|
||||||
break;
|
break;
|
||||||
case YandexInfo.OTP_SCHEMA_ID:
|
|
||||||
case YandexInfo.ID:
|
case YandexInfo.ID:
|
||||||
YandexUtils.validateSecret(secret);
|
info = new YandexInfo(secret, _textYandexPin.getText().toString());
|
||||||
byte[] pin = CryptoUtils.toBytes(_textYandexPin.getText().toString().toCharArray());
|
|
||||||
info = new YandexInfo(secret, pin);
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||||
|
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,9 +2,9 @@ package com.beemdevelopment.aegis.crypto.otp;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
import com.beemdevelopment.aegis.crypto.CryptoUtils;
|
|
||||||
import com.beemdevelopment.aegis.encoding.Base32;
|
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;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@ -23,11 +23,13 @@ public class YAOTPTest {
|
||||||
};
|
};
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void validateYaOtp() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
public void validateYaOtp()
|
||||||
|
throws InvalidKeyException, NoSuchAlgorithmException, IOException, OtpInfoException {
|
||||||
for (Vector testCase : TEST_CASES) {
|
for (Vector testCase : TEST_CASES) {
|
||||||
|
byte[] secret = YandexInfo.parseSecret(Base32.decode(testCase.secret));
|
||||||
YAOTP otp = YAOTP.generateOTP(
|
YAOTP otp = YAOTP.generateOTP(
|
||||||
Base32.decode(testCase.secret.substring(0, 26)),
|
secret,
|
||||||
CryptoUtils.toBytes(testCase.pin.toCharArray()),
|
testCase.pin,
|
||||||
8,
|
8,
|
||||||
"HmacSHA256",
|
"HmacSHA256",
|
||||||
testCase.timestamp,
|
testCase.timestamp,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import static org.junit.Assert.assertThrows;
|
||||||
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.otp.OtpInfoException;
|
import com.beemdevelopment.aegis.otp.OtpInfoException;
|
||||||
|
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||||
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
@ -19,14 +20,14 @@ public class YandexUtilsTest {
|
||||||
|
|
||||||
@Test(expected = Test.None.class)
|
@Test(expected = Test.None.class)
|
||||||
public void testValidationOk() throws EncodingException, OtpInfoException {
|
public void testValidationOk() throws EncodingException, OtpInfoException {
|
||||||
YandexUtils.validateSecret(getBase32Vector(0));
|
YandexInfo.validateSecret(getBase32Vector(0));
|
||||||
YandexUtils.validateSecret(getBase32Vector(1));
|
YandexInfo.validateSecret(getBase32Vector(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testYandexSecretValidation() {
|
public void testYandexSecretValidation() {
|
||||||
assertThrows(OtpInfoException.class, () -> YandexUtils.validateSecret(getBase32Vector(2)));
|
assertThrows(OtpInfoException.class, () -> YandexInfo.validateSecret(getBase32Vector(2)));
|
||||||
assertThrows(OtpInfoException.class, () -> YandexUtils.validateSecret(getBase32Vector(3)));
|
assertThrows(OtpInfoException.class, () -> YandexInfo.validateSecret(getBase32Vector(3)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] getBase32Vector(int vectorIndex) throws EncodingException {
|
private byte[] getBase32Vector(int vectorIndex) throws EncodingException {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue