mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-14 14:02:49 +00:00
Add Yandex OTP support
This commit is contained in:
parent
e54ac9aba4
commit
af2bf6f683
15 changed files with 541 additions and 61 deletions
|
@ -12,6 +12,7 @@ import com.beemdevelopment.aegis.encoding.Base32;
|
||||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||||
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.ui.MainActivity;
|
import com.beemdevelopment.aegis.ui.MainActivity;
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||||
|
@ -68,7 +69,8 @@ public class OverallTest extends AegisTest {
|
||||||
generateEntry(TotpInfo.class, "Frank", "Google"),
|
generateEntry(TotpInfo.class, "Frank", "Google"),
|
||||||
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")
|
||||||
);
|
);
|
||||||
for (VaultEntry entry : entries) {
|
for (VaultEntry entry : entries) {
|
||||||
addEntry(entry);
|
addEntry(entry);
|
||||||
|
@ -171,6 +173,8 @@ public class OverallTest extends AegisTest {
|
||||||
otpType = "Steam";
|
otpType = "Steam";
|
||||||
} else if (entry.getInfo() instanceof TotpInfo) {
|
} else if (entry.getInfo() instanceof TotpInfo) {
|
||||||
otpType = "TOTP";
|
otpType = "TOTP";
|
||||||
|
} else if (entry.getInfo() instanceof YandexInfo) {
|
||||||
|
otpType = "Yandex";
|
||||||
} 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()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,18 +14,7 @@ public class HOTP {
|
||||||
|
|
||||||
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
|
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
|
||||||
throws NoSuchAlgorithmException, InvalidKeyException {
|
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
|
byte[] hash = getHash(secret, algo, counter);
|
||||||
|
|
||||||
// encode counter in big endian
|
|
||||||
byte[] counterBytes = ByteBuffer.allocate(8)
|
|
||||||
.order(ByteOrder.BIG_ENDIAN)
|
|
||||||
.putLong(counter)
|
|
||||||
.array();
|
|
||||||
|
|
||||||
// calculate the hash of the counter
|
|
||||||
Mac mac = Mac.getInstance(algo);
|
|
||||||
mac.init(key);
|
|
||||||
byte[] hash = mac.doFinal(counterBytes);
|
|
||||||
|
|
||||||
// truncate hash to get the HTOP value
|
// truncate hash to get the HTOP value
|
||||||
// http://tools.ietf.org/html/rfc4226#section-5.4
|
// http://tools.ietf.org/html/rfc4226#section-5.4
|
||||||
|
@ -37,4 +26,20 @@ public class HOTP {
|
||||||
|
|
||||||
return new OTP(otp, digits);
|
return new OTP(otp, digits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] getHash(byte[] secret, String algo, long counter)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException {
|
||||||
|
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
|
||||||
|
|
||||||
|
// encode counter in big endian
|
||||||
|
byte[] counterBytes = ByteBuffer.allocate(8)
|
||||||
|
.order(ByteOrder.BIG_ENDIAN)
|
||||||
|
.putLong(counter)
|
||||||
|
.array();
|
||||||
|
|
||||||
|
// calculate the hash of the counter
|
||||||
|
Mac mac = Mac.getInstance(algo);
|
||||||
|
mac.init(key);
|
||||||
|
return mac.doFinal(counterBytes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
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.security.InvalidKeyException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class YAOTP {
|
||||||
|
private static final int EN_ALPHABET_LENGTH = 26;
|
||||||
|
private final long _code;
|
||||||
|
private final int _digits;
|
||||||
|
|
||||||
|
private YAOTP(long code, int digits) {
|
||||||
|
_code = code;
|
||||||
|
_digits = digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static YAOTP generateOTP(byte[] secret, byte[] 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)
|
||||||
|
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
|
||||||
|
|
||||||
|
long counter = (long) Math.floor((double) seconds / period);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
long code = _code % (long) Math.pow(EN_ALPHABET_LENGTH, _digits);
|
||||||
|
char[] chars = new char[_digits];
|
||||||
|
|
||||||
|
for (int i = _digits - 1; i >= 0; i--) {
|
||||||
|
chars[i] = (char) ('a' + (code % EN_ALPHABET_LENGTH));
|
||||||
|
code /= EN_ALPHABET_LENGTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String(chars);
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,42 +26,6 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
_issuer = issuer;
|
_issuer = issuer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public OtpInfo getOtpInfo() {
|
|
||||||
return _info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri getUri() {
|
|
||||||
Uri.Builder builder = new Uri.Builder();
|
|
||||||
builder.scheme(SCHEME);
|
|
||||||
|
|
||||||
if (_info instanceof TotpInfo) {
|
|
||||||
if (_info instanceof SteamInfo) {
|
|
||||||
builder.authority("steam");
|
|
||||||
} 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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.appendQueryParameter("digits", Integer.toString(_info.getDigits()));
|
|
||||||
builder.appendQueryParameter("algorithm", _info.getAlgorithm(false));
|
|
||||||
builder.appendQueryParameter("secret", Base32.encode(_info.getSecret()));
|
|
||||||
|
|
||||||
if (_issuer != null && !_issuer.equals("")) {
|
|
||||||
builder.path(String.format("%s:%s", _issuer, _accountName));
|
|
||||||
builder.appendQueryParameter("issuer", _issuer);
|
|
||||||
} else {
|
|
||||||
builder.path(_accountName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
|
public static GoogleAuthInfo parseUri(String s) throws GoogleAuthInfoException {
|
||||||
Uri uri = Uri.parse(s);
|
Uri uri = Uri.parse(s);
|
||||||
if (uri == null) {
|
if (uri == null) {
|
||||||
|
@ -84,12 +48,13 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
|
|
||||||
byte[] secret;
|
byte[] secret;
|
||||||
try {
|
try {
|
||||||
secret = parseSecret(encodedSecret);
|
secret = uri.getHost().equals(YandexInfo.OTP_SCHEMA_ID) ? parseYandexSecret(encodedSecret) : parseSecret(encodedSecret);
|
||||||
} catch (EncodingException e) {
|
} catch (EncodingException e) {
|
||||||
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
throw new GoogleAuthInfoException(uri, "Bad secret", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
OtpInfo info;
|
OtpInfo info;
|
||||||
|
String issuer = "";
|
||||||
try {
|
try {
|
||||||
String type = uri.getHost();
|
String type = uri.getHost();
|
||||||
if (type == null) {
|
if (type == null) {
|
||||||
|
@ -122,10 +87,15 @@ 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:
|
||||||
|
String pinValue = uri.getQueryParameter("pin");
|
||||||
|
info = pinValue != null ? new YandexInfo(secret, parseSecret(pinValue)) : new YandexInfo(secret);
|
||||||
|
issuer = info.getType();
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
} catch (OtpInfoException | NumberFormatException e) {
|
} catch (OtpInfoException | NumberFormatException | EncodingException e) {
|
||||||
throw new GoogleAuthInfoException(uri, e);
|
throw new GoogleAuthInfoException(uri, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +104,6 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
String label = path != null && path.length() > 0 ? path.substring(1) : "";
|
String label = path != null && path.length() > 0 ? path.substring(1) : "";
|
||||||
|
|
||||||
String accountName = "";
|
String accountName = "";
|
||||||
String issuer = "";
|
|
||||||
|
|
||||||
if (label.contains(":")) {
|
if (label.contains(":")) {
|
||||||
// a label can only contain one colon
|
// a label can only contain one colon
|
||||||
|
@ -151,7 +120,9 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
// label only contains the account name
|
// label only contains the account name
|
||||||
// grab the issuer's info from the 'issuer' parameter if it's present
|
// grab the issuer's info from the 'issuer' parameter if it's present
|
||||||
String issuerParam = uri.getQueryParameter("issuer");
|
String issuerParam = uri.getQueryParameter("issuer");
|
||||||
issuer = issuerParam != null ? issuerParam : "";
|
if (issuer.isEmpty()) {
|
||||||
|
issuer = issuerParam != null ? issuerParam : "";
|
||||||
|
}
|
||||||
accountName = label;
|
accountName = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +151,19 @@ 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) {
|
||||||
|
@ -260,7 +244,7 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
default:
|
default:
|
||||||
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
throw new GoogleAuthInfoException(uri, String.format("Unsupported algorithm: %d", params.getType().ordinal()));
|
||||||
}
|
}
|
||||||
} catch (OtpInfoException e){
|
} catch (OtpInfoException e) {
|
||||||
throw new GoogleAuthInfoException(uri, e);
|
throw new GoogleAuthInfoException(uri, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,6 +263,48 @@ public class GoogleAuthInfo implements Serializable {
|
||||||
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
|
return new Export(infos, payload.getBatchId(), payload.getBatchIndex(), payload.getBatchSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OtpInfo getOtpInfo() {
|
||||||
|
return _info;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.OTP_SCHEMA_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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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).getPinBytes()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_issuer != null && !_issuer.equals("")) {
|
||||||
|
builder.path(String.format("%s:%s", _issuer, _accountName));
|
||||||
|
builder.appendQueryParameter("issuer", _issuer);
|
||||||
|
} else {
|
||||||
|
builder.path(_accountName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
public String getIssuer() {
|
public String getIssuer() {
|
||||||
return _issuer;
|
return _issuer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,6 +115,10 @@ public abstract class OtpInfo implements Serializable {
|
||||||
case HotpInfo.ID:
|
case HotpInfo.ID:
|
||||||
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
|
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
|
||||||
break;
|
break;
|
||||||
|
case YandexInfo.ID:
|
||||||
|
byte[] pin = Base32.decode(obj.getString("pin"));
|
||||||
|
info = new YandexInfo(secret, pin);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new OtpInfoException("unsupported otp type: " + type);
|
throw new OtpInfoException("unsupported otp type: " + type);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package com.beemdevelopment.aegis.otp;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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 String ID = "yandex";
|
||||||
|
public static final String OTP_SCHEMA_ID = "yaotp";
|
||||||
|
|
||||||
|
private byte[] _pin;
|
||||||
|
|
||||||
|
public YandexInfo(byte[] secret) throws OtpInfoException {
|
||||||
|
super(secret, DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public YandexInfo(byte[] secret, byte[] pin) throws OtpInfoException {
|
||||||
|
super(secret, DEFAULT_ALGORITHM, DIGITS, TotpInfo.DEFAULT_PERIOD);
|
||||||
|
this._pin = pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getOtp() {
|
||||||
|
try {
|
||||||
|
YAOTP otp = YAOTP.generateOTP(getSecret(), _pin, getDigits(), getAlgorithm(true), getPeriod());
|
||||||
|
return otp.toString();
|
||||||
|
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPin() {
|
||||||
|
return _pin != null ? new String(_pin, StandardCharsets.UTF_8) : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPinBytes() {
|
||||||
|
return _pin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getTypeId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getType() {
|
||||||
|
String id = getTypeId();
|
||||||
|
return id.substring(0, 1).toUpperCase(Locale.ROOT) + id.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject toJson() {
|
||||||
|
JSONObject result = super.toJson();
|
||||||
|
try {
|
||||||
|
result.put("pin", Base32.encode(getPinBytes()));
|
||||||
|
} catch (JSONException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof YandexInfo)) return false;
|
||||||
|
|
||||||
|
YandexInfo that = (YandexInfo) o;
|
||||||
|
return super.equals(o) && Arrays.equals(_pin, that._pin);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return super.hashCode() + Arrays.hashCode(_pin);
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,6 +32,7 @@ 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;
|
||||||
|
@ -46,6 +47,7 @@ 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;
|
||||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||||
|
import com.beemdevelopment.aegis.otp.YandexInfo;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||||
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
||||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||||
|
@ -53,6 +55,7 @@ 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;
|
||||||
|
@ -101,6 +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 LinearLayout _textYandexPinLayout;
|
||||||
private TextInputEditText _textUsageCount;
|
private TextInputEditText _textUsageCount;
|
||||||
private TextInputEditText _textNote;
|
private TextInputEditText _textNote;
|
||||||
|
|
||||||
|
@ -153,6 +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);
|
||||||
|
_textYandexPinLayout = findViewById(R.id.layout_yandex_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);
|
||||||
|
@ -166,12 +173,20 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
|
|
||||||
// if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings
|
// if this is NOT a manually entered entry, move the "Secret" field from basic to advanced settings
|
||||||
if (!_isNew || (_isNew && !_isManual)) {
|
if (!_isNew || (_isNew && !_isManual)) {
|
||||||
|
int secretIndex = 0;
|
||||||
LinearLayout layoutSecret = findViewById(R.id.layout_secret);
|
LinearLayout layoutSecret = findViewById(R.id.layout_secret);
|
||||||
LinearLayout layoutBasic = findViewById(R.id.layout_basic);
|
LinearLayout layoutBasic = findViewById(R.id.layout_basic);
|
||||||
LinearLayout layoutAdvanced = findViewById(R.id.layout_advanced);
|
LinearLayout layoutAdvanced = findViewById(R.id.layout_advanced);
|
||||||
layoutBasic.removeView(layoutSecret);
|
layoutBasic.removeView(layoutSecret);
|
||||||
layoutAdvanced.addView(layoutSecret, 0);
|
if (!_isNew) {
|
||||||
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
|
secretIndex = 1;
|
||||||
|
layoutBasic.removeView(_textYandexPinLayout);
|
||||||
|
layoutAdvanced.addView(_textYandexPinLayout, 0);
|
||||||
|
((LinearLayout.LayoutParams) _textYandexPinLayout.getLayoutParams()).topMargin = 0;
|
||||||
|
} else {
|
||||||
|
((LinearLayout.LayoutParams) layoutSecret.getLayoutParams()).topMargin = 0;
|
||||||
|
}
|
||||||
|
layoutAdvanced.addView(layoutSecret, secretIndex);
|
||||||
|
|
||||||
if (_isNew && !_isManual) {
|
if (_isNew && !_isManual) {
|
||||||
setViewEnabled(layoutAdvanced, false);
|
setViewEnabled(layoutAdvanced, false);
|
||||||
|
@ -206,6 +221,7 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
_textNote.setText(_origEntry.getNote());
|
_textNote.setText(_origEntry.getNote());
|
||||||
|
|
||||||
OtpInfo info = _origEntry.getInfo();
|
OtpInfo info = _origEntry.getInfo();
|
||||||
|
|
||||||
if (info instanceof TotpInfo) {
|
if (info instanceof TotpInfo) {
|
||||||
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
||||||
_textPeriodCounter.setText(Integer.toString(((TotpInfo) info).getPeriod()));
|
_textPeriodCounter.setText(Integer.toString(((TotpInfo) info).getPeriod()));
|
||||||
|
@ -225,7 +241,13 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
|
|
||||||
_dropdownType.setText(_origEntry.getInfo().getType(), false);
|
_dropdownType.setText(_origEntry.getInfo().getType(), false);
|
||||||
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
|
_dropdownAlgo.setText(_origEntry.getInfo().getAlgorithm(false), false);
|
||||||
|
|
||||||
|
if (info instanceof YandexInfo) {
|
||||||
|
_textYandexPin.setText(((YandexInfo) info).getPin());
|
||||||
|
}
|
||||||
|
|
||||||
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
updateAdvancedFieldStatus(_origEntry.getInfo().getTypeId());
|
||||||
|
updatePinFieldVisibility(_origEntry.getInfo().getTypeId());
|
||||||
|
|
||||||
String group = _origEntry.getGroup();
|
String group = _origEntry.getGroup();
|
||||||
setGroup(group);
|
setGroup(group);
|
||||||
|
@ -254,11 +276,18 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
_textPeriodCounter.setText(String.valueOf(HotpInfo.DEFAULT_COUNTER));
|
_textPeriodCounter.setText(String.valueOf(HotpInfo.DEFAULT_COUNTER));
|
||||||
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
|
_textDigits.setText(String.valueOf(OtpInfo.DEFAULT_DIGITS));
|
||||||
break;
|
break;
|
||||||
|
case YandexInfo.ID:
|
||||||
|
_dropdownAlgo.setText(YandexInfo.DEFAULT_ALGORITHM, false);
|
||||||
|
_textPeriodCounterLayout.setHint(R.string.period_hint);
|
||||||
|
_textPeriodCounter.setText(String.valueOf(TotpInfo.DEFAULT_PERIOD));
|
||||||
|
_textDigits.setText(String.valueOf(YandexInfo.DIGITS));
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAdvancedFieldStatus(type);
|
updateAdvancedFieldStatus(type);
|
||||||
|
updatePinFieldVisibility(type);
|
||||||
});
|
});
|
||||||
|
|
||||||
_iconView.setOnClickListener(v -> {
|
_iconView.setOnClickListener(v -> {
|
||||||
|
@ -290,12 +319,17 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAdvancedFieldStatus(String otpType) {
|
private void updateAdvancedFieldStatus(String otpType) {
|
||||||
boolean enabled = !otpType.equals(SteamInfo.ID) && (!_isNew || _isManual);
|
boolean enabled = !otpType.equals(SteamInfo.ID) && !otpType.equals(YandexInfo.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) {
|
||||||
|
boolean visible = otpType.equals(YandexInfo.ID);
|
||||||
|
_textYandexPinLayout.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
private void setGroup(String groupName) {
|
private void setGroup(String groupName) {
|
||||||
int pos = 0;
|
int pos = 0;
|
||||||
if (groupName != null) {
|
if (groupName != null) {
|
||||||
|
@ -627,6 +661,14 @@ public class EditEntryActivity extends AegisActivity {
|
||||||
|
|
||||||
String type = _dropdownType.getText().toString();
|
String type = _dropdownType.getText().toString();
|
||||||
String algo = _dropdownAlgo.getText().toString();
|
String algo = _dropdownAlgo.getText().toString();
|
||||||
|
String lowerCasedType = type.toLowerCase(Locale.ROOT);
|
||||||
|
|
||||||
|
if (lowerCasedType.equals(YandexInfo.ID)) {
|
||||||
|
int pinLength = _textYandexPin.length();
|
||||||
|
if (pinLength < 4) {
|
||||||
|
throw new ParseException("PIN is a required field. Min 4 digits.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int digits;
|
int digits;
|
||||||
try {
|
try {
|
||||||
|
@ -664,6 +706,12 @@ 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:
|
||||||
|
YandexUtils.validateSecret(secret);
|
||||||
|
byte[] pin = CryptoUtils.toBytes(_textYandexPin.getText().toString().toCharArray());
|
||||||
|
info = new YandexInfo(secret, pin);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
throw new RuntimeException(String.format("Unsupported OTP type: %s", type));
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||||
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.ui.glide.IconLoader;
|
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
|
@ -238,7 +239,7 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
||||||
OtpInfo info = _entry.getInfo();
|
OtpInfo info = _entry.getInfo();
|
||||||
|
|
||||||
String otp = info.getOtp();
|
String otp = info.getOtp();
|
||||||
if (!(info instanceof SteamInfo)) {
|
if (!(info instanceof SteamInfo || info instanceof YandexInfo)) {
|
||||||
otp = formatCode(otp);
|
otp = formatCode(otp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
app/src/main/res/drawable/ic_baseline_fiber_pin_24.xml
Normal file
9
app/src/main/res/drawable/ic_baseline_fiber_pin_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M5.5,10.5h2v1h-2zM20,4L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,6c0,-1.11 -0.89,-2 -2,-2zM9,11.5c0,0.85 -0.65,1.5 -1.5,1.5h-2v2L4,15L4,9h3.5c0.85,0 1.5,0.65 1.5,1.5v1zM12.5,15L11,15L11,9h1.5v6zM20,15h-1.2l-2.55,-3.5L16.25,15L15,15L15,9h1.25l2.5,3.5L18.75,9L20,9v6z"/>
|
||||||
|
</vector>
|
|
@ -45,13 +45,12 @@
|
||||||
android:id="@+id/krop_view"
|
android:id="@+id/krop_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerInParent="true"
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
app:krop_aspectX="1"
|
app:krop_aspectX="1"
|
||||||
app:krop_aspectY="1"
|
app:krop_aspectY="1"
|
||||||
app:krop_offset="70dp"
|
app:krop_offset="70dp"
|
||||||
app:krop_overlayColor="#aadddddd" >
|
app:krop_overlayColor="#aadddddd">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/iv_saveImage"
|
android:id="@+id/iv_saveImage"
|
||||||
|
@ -186,6 +185,39 @@
|
||||||
android:inputType="textPassword"/>
|
android:inputType="textPassword"/>
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/layout_yandex_pin"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:visibility="visible">
|
||||||
|
<ImageView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_fiber_pin_24"
|
||||||
|
app:tint="?attr/iconColorPrimary"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginEnd="15dp"
|
||||||
|
android:layout_gravity="center_vertical"/>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/yandex_pin"
|
||||||
|
android:layout_weight="1"
|
||||||
|
app:passwordToggleTint="#949494"
|
||||||
|
app:passwordToggleEnabled="true">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/text_yandex_pin"
|
||||||
|
android:maxLength="16"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="numberPassword"/>
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
<item>TOTP</item>
|
<item>TOTP</item>
|
||||||
<item>HOTP</item>
|
<item>HOTP</item>
|
||||||
<item>Steam</item>
|
<item>Steam</item>
|
||||||
|
<item>Yandex</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="otp_algo_array">
|
<string-array name="otp_algo_array">
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
<string name="discard">Discard</string>
|
<string name="discard">Discard</string>
|
||||||
<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="suggested">Suggested</string>
|
<string name="suggested">Suggested</string>
|
||||||
<string name="usage_count">Usage count</string>
|
<string name="usage_count">Usage count</string>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
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 org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
public class YAOTPTest {
|
||||||
|
|
||||||
|
private static final Vector[] TEST_CASES = new Vector[]{
|
||||||
|
new Vector("5239", "6SB2IKNM6OBZPAVBVTOHDKS4FAAAAAAADFUTQMBTRY", 1641559648L, "umozdicq"),
|
||||||
|
new Vector("7586", "LA2V6KMCGYMWWVEW64RNP3JA3IAAAAAAHTSG4HRZPI", 1581064020L, "oactmacq"),
|
||||||
|
new Vector("7586", "LA2V6KMCGYMWWVEW64RNP3JA3IAAAAAAHTSG4HRZPI", 1581090810L, "wemdwrix"),
|
||||||
|
new Vector("5210481216086702", "JBGSAU4G7IEZG6OY4UAXX62JU4AAAAAAHTSG4HXU3M", 1581091469L, "dfrpywob"),
|
||||||
|
new Vector("5210481216086702", "JBGSAU4G7IEZG6OY4UAXX62JU4AAAAAAHTSG4HXU3M", 1581093059L, "vunyprpd"),
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void validateYaOtp() throws InvalidKeyException, NoSuchAlgorithmException, IOException {
|
||||||
|
for (Vector testCase : TEST_CASES) {
|
||||||
|
YAOTP otp = YAOTP.generateOTP(
|
||||||
|
Base32.decode(testCase.secret.substring(0, 26)),
|
||||||
|
CryptoUtils.toBytes(testCase.pin.toCharArray()),
|
||||||
|
8,
|
||||||
|
"HmacSHA256",
|
||||||
|
testCase.timestamp,
|
||||||
|
30
|
||||||
|
);
|
||||||
|
assertEquals(testCase.expected, otp.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Vector {
|
||||||
|
public String pin;
|
||||||
|
public String secret;
|
||||||
|
public long timestamp;
|
||||||
|
public String expected;
|
||||||
|
|
||||||
|
public Vector(String pin, String secret, long timestamp, String expected) {
|
||||||
|
this.pin = pin;
|
||||||
|
this.secret = secret;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.expected = expected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.beemdevelopment.aegis.util;
|
||||||
|
|
||||||
|
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 org.junit.Test;
|
||||||
|
|
||||||
|
public class YandexUtilsTest {
|
||||||
|
|
||||||
|
private static final String[] vectors = new String[]{
|
||||||
|
"LA2V6KMCGYMWWVEW64RNP3JA3IAAAAAAHTSG4HRZPI", // correct
|
||||||
|
"LA2V6KMCGYMWWVEW64RNP3JA3I", // secret from QR - no validation
|
||||||
|
"AA2V6KMCGYMWWVEW64RNP3JA3IAAAAAAHTSG4HRZPI", // first letter is different
|
||||||
|
"AA2V6KMCGJA3IAAAAAAHTSG4HRZPI" // size is wrong
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test(expected = Test.None.class)
|
||||||
|
public void testValidationOk() throws EncodingException, OtpInfoException {
|
||||||
|
YandexUtils.validateSecret(getBase32Vector(0));
|
||||||
|
YandexUtils.validateSecret(getBase32Vector(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testYandexSecretValidation() {
|
||||||
|
assertThrows(OtpInfoException.class, () -> YandexUtils.validateSecret(getBase32Vector(2)));
|
||||||
|
assertThrows(OtpInfoException.class, () -> YandexUtils.validateSecret(getBase32Vector(3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getBase32Vector(int vectorIndex) throws EncodingException {
|
||||||
|
return Base32.decode(vectors[vectorIndex]);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue