mirror of
https://github.com/Kunzisoft/KeePassDX.git
synced 2025-12-04 15:49:33 +01:00
Add OTP and fix TOTP generation
This commit is contained in:
@@ -50,7 +50,7 @@ import com.kunzisoft.keepass.timeout.TimeoutHelper
|
||||
import com.kunzisoft.keepass.utils.MenuUtil
|
||||
import com.kunzisoft.keepass.utils.UriUtil
|
||||
import com.kunzisoft.keepass.view.EntryContentsView
|
||||
import com.kunzisoft.keepass.totp.TotpSettings
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
import java.util.*
|
||||
|
||||
class EntryActivity : LockingHideActivity() {
|
||||
@@ -67,7 +67,7 @@ class EntryActivity : LockingHideActivity() {
|
||||
private var mIsHistory: Boolean = false
|
||||
|
||||
private var mShowPassword: Boolean = false
|
||||
private var mTotpSettings: TotpSettings? = null
|
||||
private var mOtpEntryFields: OtpEntryFields? = null
|
||||
|
||||
private var clipboardHelper: ClipboardHelper? = null
|
||||
private var firstLaunchOfActivity: Boolean = false
|
||||
@@ -136,6 +136,9 @@ class EntryActivity : LockingHideActivity() {
|
||||
mEntry?.touch(modified = false, touchParents = false)
|
||||
|
||||
mEntry?.let { entry ->
|
||||
// Init OTP
|
||||
mOtpEntryFields = OtpEntryFields(entry)
|
||||
|
||||
// Fill data in resume to update from EntryEditActivity
|
||||
fillEntryDataInContentsView(entry)
|
||||
// Refresh Menu
|
||||
@@ -152,9 +155,6 @@ class EntryActivity : LockingHideActivity() {
|
||||
MagikIME.addEntryAndLaunchNotificationIfAllowed(this, entryInfo)
|
||||
}
|
||||
}
|
||||
|
||||
// Init TOTP
|
||||
mTotpSettings = TotpSettings(entry)
|
||||
}
|
||||
|
||||
firstLaunchOfActivity = false
|
||||
@@ -225,10 +225,10 @@ class EntryActivity : LockingHideActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
mTotpSettings?.let { totpSettings ->
|
||||
entryContentsView?.assignTotp(totpSettings, View.OnClickListener {
|
||||
mOtpEntryFields?.let { otpEntryFields ->
|
||||
entryContentsView?.assignTotp(otpEntryFields, View.OnClickListener {
|
||||
clipboardHelper?.timeoutCopyToClipboard(
|
||||
totpSettings.token,
|
||||
otpEntryFields.token,
|
||||
getString(R.string.copy_field, getString(R.string.entry_totp))
|
||||
)
|
||||
})
|
||||
|
||||
358
app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.java
Normal file
358
app/src/main/java/com/kunzisoft/keepass/otp/OtpEntryFields.java
Normal file
@@ -0,0 +1,358 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with KeePass DX. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This code is based on KeePassXC code
|
||||
* https://github.com/keepassxreboot/keepassxc/blob/master/src/totp/totp.cpp
|
||||
* https://github.com/keepassxreboot/keepassxc/blob/master/src/core/Entry.cpp
|
||||
*/
|
||||
package com.kunzisoft.keepass.otp;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned;
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString;
|
||||
|
||||
public class OtpEntryFields {
|
||||
|
||||
private static final String TAG = OtpEntryFields.class.getName();
|
||||
|
||||
private static final String OTP_SCHEME = "otpauth";
|
||||
|
||||
private static final String TOTP = "totp"; // time-based
|
||||
private static final String HOTP = "hotp"; // counter-based
|
||||
|
||||
// URL parameters
|
||||
private static final String ISSUER_URL_PARAM = "issuer";
|
||||
private static final String SECRET_URL_PARAM = "secret";
|
||||
private static final String DIGITS_URL_PARAM = "digits";
|
||||
private static final String PERIOD_URL_PARAM = "period";
|
||||
private static final String ENCODER_URL_PARAM = "encoder";
|
||||
private static final String COUNTER_URL_PARAM = "counter";
|
||||
|
||||
// Key-values
|
||||
private static final String SEED_KEY = "key";
|
||||
private static final String DIGITS_KEY = "size";
|
||||
private static final String STEP_KEY = "step";
|
||||
|
||||
// Default values
|
||||
private static final int DEFAULT_HOTP_COUNTER = 0;
|
||||
|
||||
public enum OtpType {
|
||||
UNDEFINED,
|
||||
HOTP, // counter based
|
||||
TOTP // time based
|
||||
}
|
||||
|
||||
private enum TokenType {
|
||||
Default(6),
|
||||
Steam(5);
|
||||
|
||||
public int digits;
|
||||
|
||||
TokenType(int digits) {
|
||||
this.digits = digits;
|
||||
}
|
||||
|
||||
public static TokenType getFromString(@Nullable String tokenType) {
|
||||
if (tokenType == null)
|
||||
return Default;
|
||||
switch (tokenType) {
|
||||
case "S":
|
||||
case "steam":
|
||||
return Steam;
|
||||
default:
|
||||
return Default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final int DEFAULT_STEP = 30;
|
||||
|
||||
// Logical breakdown of key=value regex. the final string is as follows:
|
||||
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
|
||||
private static final String validKeyValue = "[^&=\\s]+";
|
||||
private static final String validKeyValuePair = validKeyValue + "=" + validKeyValue;
|
||||
private static final String validKeyValueRegex =
|
||||
validKeyValuePair + "&(" + validKeyValuePair + ")*";
|
||||
|
||||
private static final String OTP_FIELD = "otp";
|
||||
private static final String TOTP_SEED_FIELD = "TOTP Seed";
|
||||
private static final String TOTP_SETTING_FIELD = "TOTP Settings";
|
||||
|
||||
private EntryVersioned entry;
|
||||
|
||||
private OtpType type = OtpType.UNDEFINED; // ie : HOTP or TOTP
|
||||
private TokenType tokenType = TokenType.Default; // ie : default or Steam
|
||||
private String name = ""; // ie : user@email.com
|
||||
private String issuer = ""; // ie : Gitlab
|
||||
private String secretString = "";
|
||||
private byte[] secret;
|
||||
private int counter = DEFAULT_HOTP_COUNTER; // ie : 5 - only for HOTP
|
||||
private int step = 30; // ie : 30 seconds - only for TOTP
|
||||
private int digits = TokenType.Default.digits; // ie : 6 - number of digits generated
|
||||
|
||||
public OtpEntryFields(EntryVersioned entry) {
|
||||
this.entry = entry;
|
||||
if (!parseOtpFromUrl()) {
|
||||
parseTOTPFromField();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConfigured() {
|
||||
return type != OtpType.UNDEFINED;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
switch (type) {
|
||||
case HOTP:
|
||||
return OtpTokenGenerator.HOTP(secret, counter, digits);
|
||||
case TOTP:
|
||||
switch (tokenType) {
|
||||
case Steam:
|
||||
return OtpTokenGenerator.TOTP_Steam(secret, step, digits);
|
||||
case Default:
|
||||
default:
|
||||
return OtpTokenGenerator.TOTP_RFC6238(secret, step, digits);
|
||||
}
|
||||
case UNDEFINED:
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public int getSecondsRemaining() {
|
||||
return step - (int) ((System.currentTimeMillis() / 1000) % step);
|
||||
}
|
||||
|
||||
public boolean shouldRefreshToken() {
|
||||
return getSecondsRemaining() == step;
|
||||
}
|
||||
|
||||
public void setSettings(@NonNull String seed, int digits, int step) {
|
||||
// TODO: Implement a way to set TOTP from device
|
||||
}
|
||||
|
||||
private void setName(@NonNull String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
private void setIssuer(@NonNull String issuer) {
|
||||
this.issuer = issuer;
|
||||
}
|
||||
|
||||
private void setSecret(@NonNull String secret) {
|
||||
this.secret = new Base32().decode(secret.getBytes());
|
||||
}
|
||||
|
||||
private void setCounter(int counter) {
|
||||
if (counter < 0) {
|
||||
this.counter = counter;
|
||||
} else {
|
||||
this.counter = counter;
|
||||
}
|
||||
}
|
||||
|
||||
private void setStep(int step) {
|
||||
if (step <= 0 || step > 60) {
|
||||
this.step = DEFAULT_STEP;
|
||||
} else {
|
||||
this.step = step;
|
||||
}
|
||||
}
|
||||
|
||||
private void setDigits(int digits) {
|
||||
if (digits <= 0) {
|
||||
this.digits = TokenType.Default.digits;
|
||||
} else {
|
||||
this.digits = digits;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a secret value from a URI. The format will be:
|
||||
*
|
||||
* <p>otpauth://totp/user@example.com?secret=FFF...
|
||||
*
|
||||
* <p>otpauth://hotp/user@example.com?secret=FFF...&counter=123
|
||||
*
|
||||
* @param uri The URI containing the secret key
|
||||
*/
|
||||
private boolean parseOtpUri(Uri uri) {
|
||||
if (uri.getScheme() == null
|
||||
|| !OTP_SCHEME.equals(uri.getScheme().toLowerCase())) {
|
||||
Log.e(TAG, "Invalid or missing scheme in uri");
|
||||
return false;
|
||||
}
|
||||
|
||||
final String authority = uri.getAuthority();
|
||||
if (TOTP.equals(authority)) {
|
||||
|
||||
type = OtpType.TOTP;
|
||||
|
||||
} else if (HOTP.equals(authority)) {
|
||||
|
||||
type = OtpType.HOTP;
|
||||
String counterParameter = uri.getQueryParameter(COUNTER_URL_PARAM);
|
||||
if (counterParameter != null) {
|
||||
try {
|
||||
counter = Integer.parseInt(counterParameter);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "Invalid counter in uri");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(TAG, "Invalid or missing authority in uri");
|
||||
return false;
|
||||
}
|
||||
|
||||
String nameParam = validateAndGetNameInPath(uri.getPath());
|
||||
if (nameParam != null && !nameParam.isEmpty())
|
||||
setName(nameParam);
|
||||
|
||||
String issuerParam = uri.getQueryParameter(ISSUER_URL_PARAM);
|
||||
if (issuerParam != null && !issuerParam.isEmpty())
|
||||
setIssuer(issuerParam);
|
||||
|
||||
String secretParam = uri.getQueryParameter(SECRET_URL_PARAM);
|
||||
if (secretParam != null && !secretParam.isEmpty())
|
||||
setSecret(secretParam);
|
||||
|
||||
String encoderParam = uri.getQueryParameter(ENCODER_URL_PARAM);
|
||||
if (encoderParam != null && !encoderParam.isEmpty())
|
||||
setDigits(TokenType.getFromString(encoderParam).digits);
|
||||
|
||||
String digitsParam = uri.getQueryParameter(DIGITS_URL_PARAM);
|
||||
if (digitsParam != null && !digitsParam.isEmpty())
|
||||
setDigits(toInt(digitsParam));
|
||||
|
||||
String counterParam = uri.getQueryParameter(COUNTER_URL_PARAM);
|
||||
if (counterParam != null && !counterParam.isEmpty())
|
||||
setCounter(toInt(counterParam));
|
||||
|
||||
String stepParam = uri.getQueryParameter(PERIOD_URL_PARAM);
|
||||
if (stepParam != null && !stepParam.isEmpty())
|
||||
setStep(toInt(stepParam));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String validateAndGetNameInPath(String path) {
|
||||
if (path == null || !path.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
// path is "/name", so remove leading "/", and trailing white spaces
|
||||
String name = path.substring(1).trim();
|
||||
if (name.length() == 0) {
|
||||
return null; // only white spaces.
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private boolean parseOtpKeyValues(String plainText) {
|
||||
if (Pattern.matches(validKeyValueRegex, plainText)) {
|
||||
// KeeOtp string format
|
||||
HashMap<String, String> query = breakDownKeyValuePairs(plainText);
|
||||
|
||||
String secretString = query.get(SEED_KEY);
|
||||
if (secretString == null)
|
||||
secretString = "";
|
||||
setSecret(secretString);
|
||||
setDigits(toInt(query.get(DIGITS_KEY)));
|
||||
setStep(toInt(query.get(STEP_KEY)));
|
||||
return true;
|
||||
} else {
|
||||
// Malformed
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean parseOtpFromUrl() {
|
||||
String otpPlainText = getField(OTP_FIELD);
|
||||
if (otpPlainText == null
|
||||
|| otpPlainText.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parseOtpUri(Uri.parse(otpPlainText))
|
||||
|| parseOtpKeyValues(otpPlainText);
|
||||
}
|
||||
|
||||
private boolean parseTOTPFromField() {
|
||||
String seedField = getField(TOTP_SEED_FIELD);
|
||||
String settingsField = getField(TOTP_SETTING_FIELD);
|
||||
if (seedField == null || settingsField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
type = OtpType.TOTP;
|
||||
|
||||
// Regex match, sync with OtpTokenGenerator.shortNameToEncoder
|
||||
Pattern pattern = Pattern.compile("(\\d+);((?:\\d+)|S)");
|
||||
Matcher matcher = pattern.matcher(settingsField);
|
||||
if (!matcher.matches()) {
|
||||
// malformed
|
||||
return false;
|
||||
}
|
||||
|
||||
step = toInt(matcher.group(1));
|
||||
|
||||
String encodingType = matcher.group(2);
|
||||
digits = TokenType.getFromString(encodingType).digits;
|
||||
|
||||
secretString = seedField;
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getField(String id) {
|
||||
ProtectedString field = entry.getCustomFields().get(id);
|
||||
if (field != null) {
|
||||
return field.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int toInt(String value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, String> breakDownKeyValuePairs(String pairs) {
|
||||
String[] elements = pairs.split("&");
|
||||
HashMap<String, String> output = new HashMap<>();
|
||||
for (String element : elements) {
|
||||
String[] pair = element.split("=");
|
||||
output.put(pair[0], pair[1]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
* https://github.com/andOTP/andOTP/blob/master/app/src/main/java/org/shadowice/flocke/andotp/
|
||||
* Utilities/TokenCalculator.java
|
||||
*/
|
||||
package com.kunzisoft.keepass.totp;
|
||||
package com.kunzisoft.keepass.otp;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -27,7 +27,7 @@ import java.security.NoSuchAlgorithmException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public final class TotpGenerator {
|
||||
public final class OtpTokenGenerator {
|
||||
|
||||
private static final char[] STEAM_CHARS =
|
||||
new char[] {'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J',
|
||||
@@ -1,222 +0,0 @@
|
||||
/*
|
||||
* Copyright 2019 Jeremy Jamet / Kunzisoft.
|
||||
*
|
||||
* This file is part of KeePass DX.
|
||||
*
|
||||
* KeePass DX is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* KeePass DX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
|
||||
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with KeePass DX. If not,
|
||||
* see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* This code is based on KeePassXC code
|
||||
* https://github.com/keepassxreboot/keepassxc/blob/master/src/totp/totp.cpp
|
||||
* https://github.com/keepassxreboot/keepassxc/blob/master/src/core/Entry.cpp
|
||||
*/
|
||||
package com.kunzisoft.keepass.totp;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import android.net.Uri;
|
||||
import android.util.Patterns;
|
||||
import java.util.HashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.kunzisoft.keepass.database.element.EntryVersioned;
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString;
|
||||
|
||||
public class TotpSettings {
|
||||
|
||||
private enum EntryType {
|
||||
None, OTP, SeedAndSettings,
|
||||
}
|
||||
|
||||
private enum TokenType {
|
||||
Default, Steam
|
||||
}
|
||||
|
||||
private static final int DEFAULT_STEP = 30;
|
||||
private static final int DEFAULT_DIGITS = 6;
|
||||
private static final int STEAM_DIGITS = 5;
|
||||
|
||||
// Logical breakdown of key=value regex. the final string is as follows:
|
||||
// [^&=\s]+=[^&=\s]+(&[^&=\s]+=[^&=\s]+)*
|
||||
private static final String validKeyValue = "[^&=\\s]+";
|
||||
private static final String validKeyValuePair = validKeyValue + "=" + validKeyValue;
|
||||
private static final String validKeyValueRegex =
|
||||
validKeyValuePair + "&(" + validKeyValuePair + ")*";
|
||||
|
||||
private static final String OTP_FIELD = "otp";
|
||||
private static final String SEED_FIELD = "TOTP Seed";
|
||||
private static final String SETTING_FIELD = "TOTP Settings";
|
||||
|
||||
private EntryVersioned entry;
|
||||
private String seed;
|
||||
private byte[] secret;
|
||||
private int step;
|
||||
private int digits;
|
||||
private EntryType entryType;
|
||||
private TokenType tokenType;
|
||||
|
||||
public TotpSettings(EntryVersioned entry) {
|
||||
this.entry = entry;
|
||||
if (parseOtp() || parseSeedAndSettings()) {
|
||||
secret = new Base32().decode(seed.getBytes());
|
||||
} else {
|
||||
entryType = EntryType.None;
|
||||
}
|
||||
}
|
||||
|
||||
public void setSettings(String seed, int digits, int step) {
|
||||
// TODO: Implement a way to set TOTP from device
|
||||
}
|
||||
|
||||
public boolean isConfigured() {
|
||||
return entryType != EntryType.None;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
if (entryType == EntryType.None) {
|
||||
return "";
|
||||
}
|
||||
switch (tokenType) {
|
||||
case Steam:
|
||||
return TotpGenerator.TOTP_Steam(secret, step, digits);
|
||||
default:
|
||||
return TotpGenerator.TOTP_RFC6238(secret, step, digits);
|
||||
}
|
||||
}
|
||||
|
||||
public int getSecondsRemaining() {
|
||||
return step - (int) ((System.currentTimeMillis() / 1000) % step);
|
||||
}
|
||||
|
||||
public boolean shouldRefreshToken() {
|
||||
return getSecondsRemaining() == step;
|
||||
}
|
||||
|
||||
private boolean parseSeedAndSettings() {
|
||||
String seedField = getField(SEED_FIELD);
|
||||
String settingsField = getField(SETTING_FIELD);
|
||||
if (seedField == null || settingsField == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Regex match, sync with TotpGenerator.shortNameToEncoder
|
||||
Pattern pattern = Pattern.compile("(\\d+);((?:\\d+)|S)");
|
||||
Matcher matcher = pattern.matcher(settingsField);
|
||||
if (!matcher.matches()) {
|
||||
// malformed
|
||||
return false;
|
||||
}
|
||||
|
||||
step = toInt(matcher.group(1));
|
||||
|
||||
String encodingType = matcher.group(2);
|
||||
digits = getDigitsForType(encodingType);
|
||||
|
||||
seed = seedField;
|
||||
entryType = EntryType.SeedAndSettings;
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean parseOtp() {
|
||||
String key = getField(OTP_FIELD);
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri url = null;
|
||||
if (isValidUrl(key)) {
|
||||
url = Uri.parse(key);
|
||||
}
|
||||
boolean useEncoder = false;
|
||||
|
||||
if (url != null && url.getScheme().equals("otpauth")) {
|
||||
// Default OTP url format
|
||||
|
||||
seed = url.getQueryParameter("secret");
|
||||
digits = toInt(url.getQueryParameter("digits"));
|
||||
step = toInt(url.getQueryParameter("period"));
|
||||
|
||||
String encName = url.getQueryParameter("encoder");
|
||||
digits = getDigitsForType(encName);
|
||||
} else if (Pattern.matches(validKeyValueRegex, key)) {
|
||||
// KeeOtp string format
|
||||
HashMap<String, String> query = breakDownKeyValuePairs(key);
|
||||
|
||||
seed = query.get("key");
|
||||
digits = toInt(query.get("size"));
|
||||
step = toInt(query.get("step"));
|
||||
} else {
|
||||
// Malformed
|
||||
return false;
|
||||
}
|
||||
|
||||
if (digits == 0) {
|
||||
digits = DEFAULT_DIGITS;
|
||||
}
|
||||
|
||||
if (step <= 0 || step > 60) {
|
||||
step = DEFAULT_STEP;
|
||||
}
|
||||
|
||||
entryType = EntryType.OTP;
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getField(String id) {
|
||||
ProtectedString field = entry.getCustomFields().get(id);
|
||||
if (field != null) {
|
||||
return field.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isValidUrl(String url) {
|
||||
return Patterns.WEB_URL.matcher(url).matches();
|
||||
}
|
||||
|
||||
private int toInt(String value) {
|
||||
if (value == null) {
|
||||
return 0;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, String> breakDownKeyValuePairs(String pairs) {
|
||||
String[] elements = pairs.split("&");
|
||||
HashMap<String, String> output = new HashMap<String, String>();
|
||||
for (String element : elements) {
|
||||
String[] pair = element.split("=");
|
||||
output.put(pair[0], pair[1]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private int getDigitsForType(String encodingType) {
|
||||
int digitType = toInt(encodingType);
|
||||
if (digitType != 0) {
|
||||
tokenType = TokenType.Default;
|
||||
return digitType;
|
||||
}
|
||||
switch (encodingType) {
|
||||
case "S":
|
||||
case "steam":
|
||||
tokenType = TokenType.Steam;
|
||||
return 5;
|
||||
default:
|
||||
tokenType = TokenType.Default;
|
||||
return DEFAULT_DIGITS;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ import com.kunzisoft.keepass.database.element.EntryVersioned
|
||||
import com.kunzisoft.keepass.database.element.PwDate
|
||||
import com.kunzisoft.keepass.database.element.security.ProtectedString
|
||||
import java.util.*
|
||||
import com.kunzisoft.keepass.totp.TotpSettings
|
||||
import com.kunzisoft.keepass.otp.OtpEntryFields
|
||||
|
||||
class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
var attrs: AttributeSet? = null,
|
||||
@@ -210,27 +210,27 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
|
||||
}
|
||||
}
|
||||
|
||||
fun assignTotp(settings: TotpSettings, onClickListener: OnClickListener) {
|
||||
if (settings.isConfigured) {
|
||||
fun assignTotp(otpEntryFields: OtpEntryFields, onClickListener: OnClickListener) {
|
||||
if (otpEntryFields.isConfigured) {
|
||||
totpContainerView.visibility = View.VISIBLE
|
||||
|
||||
val totp = settings.token
|
||||
if (totp.isEmpty()) {
|
||||
val totpToken = otpEntryFields.token
|
||||
if (totpToken.isEmpty()) {
|
||||
totpView.text = context.getString(R.string.error_invalid_TOTP)
|
||||
totpActionView
|
||||
.setColorFilter(ContextCompat.getColor(context, R.color.grey_dark))
|
||||
assignTotpCopyListener(null)
|
||||
} else {
|
||||
assignTotpCopyListener(onClickListener)
|
||||
totpCurrentToken = settings.token
|
||||
totpCurrentToken = otpEntryFields.token
|
||||
val totpHandler = Handler()
|
||||
totpHandler.post(object : Runnable {
|
||||
override fun run() {
|
||||
if (settings.shouldRefreshToken()) {
|
||||
totpCurrentToken = settings.token
|
||||
if (otpEntryFields.shouldRefreshToken()) {
|
||||
totpCurrentToken = otpEntryFields.token
|
||||
}
|
||||
totpView.text = context.getString(R.string.entry_totp_format,
|
||||
totpCurrentToken, settings.secondsRemaining)
|
||||
totpCurrentToken, otpEntryFields.secondsRemaining)
|
||||
totpHandler.postDelayed(this, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user