diff options
-rw-r--r-- | app/build.gradle | 1 | ||||
-rw-r--r-- | app/src/androidTest/java/com/zeapo/pwdstore/PasswordEntryTest.java | 18 | ||||
-rw-r--r-- | app/src/androidTest/java/com/zeapo/pwdstore/TotpTest.java | 12 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/PasswordEntry.java | 19 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/crypto/PgpActivity.kt | 22 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/utils/Totp.java | 50 | ||||
-rw-r--r-- | app/src/main/res/layout/decrypt_layout.xml | 39 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 2 |
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> |