aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWiktor Kwapisiewicz <wiktor@metacode.biz>2017-11-06 12:41:00 +0100
committerMohamed Zenadi <zeapo@users.noreply.github.com>2017-11-19 11:14:45 +0100
commit3d5dd65e306ba3ca560b3c221830ee4cdb4363fb (patch)
tree6a417353098a9dc22093568c7e6c1dd505997512
parent67a7b124eec9178f8539664bf4d2528a111465b2 (diff)
Display TOTP code if entry contains OTP secret
TOTP is calculated on display and on copy to clipboard from secret embedded in entry (either in password or in extra) and the current time.
-rw-r--r--app/build.gradle1
-rw-r--r--app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java18
-rw-r--r--app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java12
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java19
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt22
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Totp.java50
-rw-r--r--app/src/main/res/layout/decrypt_layout.xml39
-rw-r--r--app/src/main/res/values/strings.xml2
8 files changed, 162 insertions, 1 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 3727389c..db6e81df 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -68,6 +68,7 @@ dependencies {
}
compile 'com.jcraft:jsch:0.1.54'
compile group: 'commons-io', name: 'commons-io', version: '2.4'
+ compile group: 'commons-codec', name: 'commons-codec', version: '1.11'
compile 'com.jayway.android.robotium:robotium-solo:5.3.1'
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
compile 'com.android.support.constraint:constraint-layout:1.0.2'
diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java b/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java
index 66636b1b..e8ddc04c 100644
--- a/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java
+++ b/app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java
@@ -41,4 +41,22 @@ public class PasswordEntryTest extends TestCase {
assertFalse(new PasswordEntry("\n").hasUsername());
assertFalse(new PasswordEntry("").hasUsername());
}
+
+ public void testNoTotpUriPresent() {
+ PasswordEntry entry = new PasswordEntry("secret\nextra\nlogin: username\ncontent");
+ assertFalse(entry.hasTotp());
+ assertNull(entry.getTotpSecret());
+ }
+
+ public void testTotpUriInPassword() {
+ PasswordEntry entry = new PasswordEntry("otpauth://totp/test?secret=JBSWY3DPEHPK3PXP");
+ assertTrue(entry.hasTotp());
+ assertEquals("JBSWY3DPEHPK3PXP", entry.getTotpSecret());
+ }
+
+ public void testTotpUriInContent() {
+ PasswordEntry entry = new PasswordEntry("secret\nusername: test\notpauth://totp/test?secret=JBSWY3DPEHPK3PXP");
+ assertTrue(entry.hasTotp());
+ assertEquals("JBSWY3DPEHPK3PXP", entry.getTotpSecret());
+ }
} \ No newline at end of file
diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java b/app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java
new file mode 100644
index 00000000..500644d1
--- /dev/null
+++ b/app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java
@@ -0,0 +1,12 @@
+package com.zeapo.pwdstore;
+
+import com.zeapo.pwdstore.utils.Totp;
+
+import junit.framework.TestCase;
+
+public class TotpTest extends TestCase {
+ public void testTotp() {
+ String code = Totp.calculateCode("JBSWY3DPEHPK3PXP", 0L);
+ assertEquals("282760", code);
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java b/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java
index d4d3fe81..ba689fe9 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java
@@ -1,5 +1,7 @@
package com.zeapo.pwdstore;
+import android.net.Uri;
+
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
@@ -13,6 +15,7 @@ public class PasswordEntry {
private final String extraContent;
private final String password;
private final String username;
+ private final String totpSecret;
public PasswordEntry(final ByteArrayOutputStream os) throws UnsupportedEncodingException {
this(os.toString("UTF-8"));
@@ -23,6 +26,7 @@ public class PasswordEntry {
password = passContent[0];
extraContent = passContent.length > 1 ? passContent[1] : "";
username = findUsername();
+ totpSecret = findTotpSecret(decryptedContent);
}
public String getPassword() {
@@ -37,6 +41,10 @@ public class PasswordEntry {
return username;
}
+ public String getTotpSecret() {
+ return totpSecret;
+ }
+
public boolean hasExtraContent() {
return extraContent.length() != 0;
}
@@ -45,6 +53,8 @@ public class PasswordEntry {
return username != null;
}
+ public boolean hasTotp() { return totpSecret != null; }
+
private String findUsername() {
final String[] extraLines = extraContent.split("\n");
for (String line : extraLines) {
@@ -56,4 +66,13 @@ public class PasswordEntry {
}
return null;
}
+
+ private String findTotpSecret(String decryptedContent) {
+ for (String line : decryptedContent.split("\n")) {
+ if (line.startsWith("otpauth://totp/")) {
+ return Uri.parse(line).getQueryParameter("secret");
+ }
+ }
+ return null;
+ }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt
index db2acfac..905f37bf 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt
@@ -20,6 +20,7 @@ import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.pwgenDialogFragment
+import com.zeapo.pwdstore.utils.Totp
import kotlinx.android.synthetic.main.decrypt_layout.*
import kotlinx.android.synthetic.main.encrypt_layout.*
import org.apache.commons.io.FileUtils
@@ -32,6 +33,7 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.charset.Charset
+import java.util.Date
class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
private val clipboard: ClipboardManager by lazy {
@@ -231,6 +233,20 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
}
}
+ if (entry.hasTotp()) {
+ crypto_totp_show.visibility = View.VISIBLE
+ crypto_totp_show_label.visibility = View.VISIBLE
+ crypto_copy_totp.visibility = View.VISIBLE
+
+ crypto_copy_totp.setOnClickListener { copyTotpToClipBoard(Totp.calculateCode(entry.totpSecret, Date().time / 1000)) }
+ crypto_totp_show.typeface = monoTypeface
+ crypto_totp_show.text = Totp.calculateCode(entry.totpSecret, Date().time / 1000);
+ } else {
+ crypto_totp_show.visibility = View.GONE
+ crypto_totp_show_label.visibility = View.GONE
+ crypto_copy_totp.visibility = View.GONE
+ }
+
if (settings.getBoolean("copy_on_decrypt", true)) {
copyPasswordToClipBoard()
}
@@ -460,6 +476,12 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
showToast(resources.getString(R.string.clipboard_username_toast_text))
}
+ private fun copyTotpToClipBoard(code: String) {
+ val clip = ClipData.newPlainText("pgp_handler_result_pm", code)
+ clipboard.primaryClip = clip
+ showToast(resources.getString(R.string.clipboard_totp_toast_text))
+ }
+
private fun shareAsPlaintext() {
if (findViewById<View>(R.id.share_password_as_plaintext) == null)
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Totp.java b/app/src/main/java/com/zeapo/pwdstore/utils/Totp.java
new file mode 100644
index 00000000..5e4326e2
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/Totp.java
@@ -0,0 +1,50 @@
+package com.zeapo.pwdstore.utils;
+
+import android.util.Log;
+
+import org.apache.commons.codec.binary.Base32;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class Totp {
+
+ private static final String ALGORITHM = "HmacSHA1";
+ private static final int TIME_WINDOW = 30;
+ private static final int CODE_DIGITS = 6;
+
+ private static final Base32 BASE_32 = new Base32();
+
+ private Totp() {
+ }
+
+ public static String calculateCode(String secret, long epochSeconds) {
+ SecretKeySpec signingKey = new SecretKeySpec(BASE_32.decode(secret), ALGORITHM);
+
+ Mac mac = null;
+ try {
+ mac = Mac.getInstance(ALGORITHM);
+ mac.init(signingKey);
+ } catch (NoSuchAlgorithmException e) {
+ Log.e("TOTP", ALGORITHM + " unavailable - should never happen", e);
+ return null;
+ } catch (InvalidKeyException e) {
+ Log.e("TOTP", "Key is malformed", e);
+ return null;
+ }
+
+ long time = epochSeconds / TIME_WINDOW;
+ byte[] digest = mac.doFinal(ByteBuffer.allocate(8).putLong(time).array());
+ int offset = digest[digest.length - 1] & 0xf;
+ byte[] code = Arrays.copyOfRange(digest, offset, offset + 4);
+ code[0] = (byte) (0x7f & code[0]);
+ String strCode = new BigInteger(code).toString();
+ return strCode.substring(strCode.length() - CODE_DIGITS);
+ }
+}
diff --git a/app/src/main/res/layout/decrypt_layout.xml b/app/src/main/res/layout/decrypt_layout.xml
index 971331a9..727b6376 100644
--- a/app/src/main/res/layout/decrypt_layout.xml
+++ b/app/src/main/res/layout/decrypt_layout.xml
@@ -148,13 +148,50 @@
android:textIsSelectable="true"
android:typeface="monospace" />
+ <ImageButton
+ android:id="@+id/crypto_copy_totp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentTop="true"
+ android:contentDescription="@string/copy_username"
+ android:background="@color/background"
+ android:src="@drawable/ic_content_copy"/>
+
+ <TextView
+ android:id="@+id/crypto_totp_show_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:layout_toLeftOf="@id/crypto_copy_totp"
+ android:layout_toStartOf="@id/crypto_copy_totp"
+ android:text="@string/totp"
+ android:textColor="@android:color/black"
+ android:textStyle="bold" />
+
+ <TextView
+ android:id="@+id/crypto_totp_show"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_below="@id/crypto_totp_show_label"
+ android:layout_toLeftOf="@id/crypto_copy_totp"
+ android:layout_toStartOf="@id/crypto_copy_totp"
+ android:textColor="@android:color/black"
+ android:textIsSelectable="true"
+ android:typeface="monospace" />
+
<TextView
android:id="@+id/crypto_extra_show_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
- android:layout_below="@id/crypto_username_show"
+ android:layout_below="@id/crypto_totp_show"
android:text="@string/extra_content"
android:textColor="@android:color/black"
android:textStyle="bold" />
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 65591586..dd286625 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -30,6 +30,7 @@
<string name="provider_toast_text">No OpenPGP Provider selected!</string>
<string name="clipboard_password_toast_text">Password copied to clipboard, you have %d seconds to paste it somewhere.</string>
<string name="clipboard_username_toast_text">Username copied to clipboard</string>
+ <string name="clipboard_totp_toast_text">TOTP code copied to clipboard</string>
<string name="file_toast_text">Please provide a file name</string>
<string name="empty_toast_text">You cannot use an empty password or empty extra content</string>
@@ -92,6 +93,7 @@
<string name="password">Password:</string>
<string name="extra_content">Extra content:</string>
<string name="username">Username:</string>
+ <string name="totp">TOTP:</string>
<string name="edit_password">Edit password</string>
<string name="copy_password">Copy password</string>
<string name="copy_username">Copy username</string>