summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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>