Merge branch 'totpReadMode' of git://github.com/studio315b/KeePassDX into studio315b-totpReadMode

This commit is contained in:
J-Jamet
2019-11-04 17:52:14 +01:00
9 changed files with 428 additions and 45 deletions

7
.gitignore vendored
View File

@@ -38,6 +38,13 @@ proguard/
# Android Studio captures folder
captures/
# Eclipse/VS Code
.project
.settings/*
*/.project
*/.classpath
*/.settings/*
# Intellij
*.iml
.idea/workspace.xml

View File

@@ -1,45 +0,0 @@
Original author:
Brian Pellin
Achim Weimert
Johan Berts - search patches
Mike Mohr - Better native code for aes and sha
Tobias Selig - icon support
Tolga Onbay, Dirk Bergstrom - password generator
Space Cowboy - holo theme
josefwells
Nicholas FitzRoy-Dale - auto launch intents
yulin2 - responsiveness improvements
Tadashi Saito
vhschlenker
bumper314 - Samsung multiwindow support
Hans Cappelle - fingerprint sensor integration
Jeremy Jamet - Keepass DX Material Design - Patches
Translations:
Diego Pierotto - Italian
Laurent, Norman Obry, Nam, Bruno Parmentier, Credomo - French
Maciej Bieniek, cod3r - Polish
Максим Сёмочкин, i.nedoboy, filimonic, bboa - Russian
MaWi, rvs2008, meviox, MaDill, EdlerProgrammierer, Jan Thomas - German
yslandro - Norwegian Nynorsk
王科峰 - Chinese
Typhoon - Slovak
Masahiro Inamura - Japanese
Matsuu Takuto - Japanese
Carlos Schlyter - Portugese (Brazil)
YSmhXQDd6Z - Portugese (Portugal)
andriykopanytsia - Ukranian
intel, Zoltán Antal - Hungarian
H Vanek - Czech
jipanos - Spanish
Erik Fdevriendt, Erik Jan Meijer - Dutch
Frederik Svarre - Danish
Oriol Garrote - Catalan
Mika Takala - Finnish
Niclas Burgren - Swedish
Raimonds - Latvian
dgarciabad - Basque
Arthur Zamarin - Hebrew
RaptorTFX - Greek
zygimantus - Lithuanian

View File

@@ -106,6 +106,8 @@ dependencies {
// Apache Commons Collections
implementation 'commons-collections:commons-collections:3.2.1'
implementation 'org.apache.commons:commons-io:1.3.2'
// Apache Commons Codec
implementation 'commons-codec:commons-codec:1.11'
// Base64
implementation 'biz.source_code:base64coder:2010-12-19'
// Icon pack

View File

@@ -49,6 +49,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
class EntryActivity : LockingHideActivity() {
@@ -59,6 +60,7 @@ class EntryActivity : LockingHideActivity() {
private var mEntry: EntryVersioned? = null
private var mShowPassword: Boolean = false
private var mTotpSettings: TotpSettings? = null
private var clipboardHelper: ClipboardHelper? = null
private var firstLaunchOfActivity: Boolean = false
@@ -97,6 +99,9 @@ class EntryActivity : LockingHideActivity() {
// Update last access time.
mEntry?.touch(modified = false, touchParents = false)
// Init TOTP
mTotpSettings = TotpSettings(mEntry)
// Retrieve the textColor to tint the icon
val taIconColor = theme.obtainStyledAttributes(intArrayOf(R.attr.colorAccent))
iconColor = taIconColor.getColor(0, Color.BLACK)
@@ -206,6 +211,15 @@ class EntryActivity : LockingHideActivity() {
}
}
mTotpSettings?.let { totpSettings ->
entryContentsView?.assignTotp(totpSettings, View.OnClickListener {
clipboardHelper?.timeoutCopyToClipboard(
totpSettings.token,
getString(R.string.copy_field, getString(R.string.entry_totp))
)
})
}
entryContentsView?.assignURL(entry.url)
entryContentsView?.assignComment(entry.notes)

View File

@@ -0,0 +1,105 @@
/*
* 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 andOTP code
* https://github.com/andOTP/andOTP/blob/master/app/src/main/java/org/shadowice/flocke/andotp/
* Utilities/TokenCalculator.java
*/
package com.kunzisoft.keepass.totp;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class TotpGenerator {
private static final char[] STEAM_CHARS =
new char[] {'2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J',
'K', 'M', 'N', 'P', 'Q', 'R', 'T', 'V', 'W', 'X', 'Y'};
private static final String ALGORITHM = "HmacSHA1";
private static byte[] generateHash(byte[] key, byte[] data)
throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(new SecretKeySpec(key, ALGORITHM));
return mac.doFinal(data);
}
public static int TOTP_RFC6238(byte[] secret, int period, long time, int digits) {
int fullToken = TOTP(secret, period, time);
int div = (int) Math.pow(10, digits);
return fullToken % div;
}
public static String TOTP_RFC6238(byte[] secret, int period, int digits) {
int token = TOTP_RFC6238(secret, period, System.currentTimeMillis() / 1000, digits);
return String.format("%0" + digits + "d", token);
}
public static String TOTP_Steam(byte[] secret, int period, int digits) {
int fullToken = TOTP(secret, period, System.currentTimeMillis() / 1000);
StringBuilder tokenBuilder = new StringBuilder();
for (int i = 0; i < digits; i++) {
tokenBuilder.append(STEAM_CHARS[fullToken % STEAM_CHARS.length]);
fullToken /= STEAM_CHARS.length;
}
return tokenBuilder.toString();
}
public static String HOTP(byte[] secret, long counter, int digits) {
int fullToken = HOTP(secret, counter);
int div = (int) Math.pow(10, digits);
return String.format("%0" + digits + "d", fullToken % div);
}
private static int TOTP(byte[] key, int period, long time) {
return HOTP(key, time / period);
}
private static int HOTP(byte[] key, long counter) {
int r = 0;
try {
byte[] data = ByteBuffer.allocate(8).putLong(counter).array();
byte[] hash = generateHash(key, data);
int offset = hash[hash.length - 1] & 0xF;
int binary = (hash[offset] & 0x7F) << 0x18;
binary |= (hash[offset + 1] & 0xFF) << 0x10;
binary |= (hash[offset + 2] & 0xFF) << 0x08;
binary |= (hash[offset + 3] & 0xFF);
r = binary;
} catch (Exception e) {
e.printStackTrace();
}
return r;
}
}

View File

@@ -0,0 +1,222 @@
/*
* 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;
}
}
}

View File

@@ -20,6 +20,7 @@ package com.kunzisoft.keepass.view
import android.content.Context
import android.graphics.Color
import android.os.Handler
import androidx.core.content.ContextCompat
import android.text.method.PasswordTransformationMethod
import android.util.AttributeSet
@@ -33,6 +34,7 @@ import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.element.security.ProtectedString
import java.text.DateFormat
import java.util.*
import com.kunzisoft.keepass.totp.TotpSettings
class EntryContentsView @JvmOverloads constructor(context: Context,
var attrs: AttributeSet? = null,
@@ -50,6 +52,11 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
private val passwordView: TextView
private val passwordActionView: ImageView
private val totpContainerView: View
private val totpView: TextView
private val totpActionView: ImageView
private var totpCurrentToken: String = ""
private val urlContainerView: View
private val urlView: TextView
@@ -87,6 +94,10 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
passwordView = findViewById(R.id.entry_password)
passwordActionView = findViewById(R.id.entry_password_action_image)
totpContainerView = findViewById(R.id.entry_totp_container);
totpView = findViewById(R.id.entry_totp);
totpActionView = findViewById(R.id.entry_totp_action_image);
urlContainerView = findViewById(R.id.entry_url_container)
urlView = findViewById(R.id.entry_url)
@@ -185,6 +196,38 @@ class EntryContentsView @JvmOverloads constructor(context: Context,
}
}
fun assignTotp(settings: TotpSettings, onClickListener: OnClickListener) {
if (settings.isConfigured) {
totpContainerView.visibility = View.VISIBLE
val totp = settings.token
if (totp.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
val totpHandler = Handler()
totpHandler.post(object : Runnable {
override fun run() {
if (settings.shouldRefreshToken()) {
totpCurrentToken = settings.token
}
totpView.text = context.getString(R.string.entry_totp_format,
totpCurrentToken, settings.secondsRemaining)
totpHandler.postDelayed(this, 1000)
}
})
}
}
}
fun assignTotpCopyListener(onClickListener: OnClickListener?) {
totpActionView.setOnClickListener(onClickListener)
}
fun assignURL(url: String?) {
if (url != null && url.isNotEmpty()) {
urlContainerView.visibility = View.VISIBLE

View File

@@ -105,6 +105,38 @@
android:tint="?attr/colorAccent" />
</RelativeLayout>
<!-- TOTP -->
<RelativeLayout
android:id="@+id/entry_totp_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_totp_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/entry_totp"
style="@style/KeepassDXStyle.TextAppearance.LabelTextStyle" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/entry_totp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/entry_totp_label"
android:layout_toLeftOf="@+id/entry_totp_action_image"
android:layout_toStartOf="@+id/entry_totp_action_image"
android:textIsSelectable="true"
style="@style/KeepassDXStyle.TextAppearance.TextEntryItem" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/entry_totp_action_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_below="@+id/entry_totp_label"
android:src="@drawable/ic_content_copy_white_24dp"
android:tint="?attr/colorAccent" />
</RelativeLayout>
<!-- URL -->
<LinearLayout
android:id="@+id/entry_url_container"

View File

@@ -87,6 +87,8 @@
<string name="entry_password">Password</string>
<string name="entry_save">Save</string>
<string name="entry_title">Title</string>
<string name="entry_totp">TOTP</string>
<string name="entry_totp_format">%1$s (%2$d)</string>
<string name="entry_url">URL</string>
<string name="entry_user_name">Username</string>
<string name="error_arc4">The ARCFOUR stream cipher is not supported.</string>
@@ -94,6 +96,7 @@
<string name="error_file_not_create">Could not create file:</string>
<string name="error_invalid_db">Could not read database.</string>
<string name="error_invalid_path">Make sure the path is correct.</string>
<string name="error_invalid_TOTP">Invalid TOTP secret.</string>
<string name="error_no_name">Enter a name.</string>
<string name="error_nokeyfile">Select a keyfile.</string>
<string name="error_out_of_memory">No memory to load your entire database.</string>