diff --git a/CHANGELOG.md b/CHANGELOG.md
index adcdc57f7..a10857c99 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file.
- TOTP support is reintroduced by popular demand. HOTP continues to be unsupported and heavily discouraged.
- Initial support for detecting and filling OTP fields with Autofill
+- Importing TOTP secrets using QR codes
## [1.9.1] - 2020-06-28
diff --git a/app/build.gradle b/app/build.gradle
index 70b1f1f78..eaef71c27 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -93,6 +93,9 @@ dependencies {
implementation deps.kotlin.coroutines.android
implementation deps.kotlin.coroutines.core
+ implementation deps.first_party.openpgp_ktx
+ implementation deps.first_party.zxing_android_embedded
+
implementation deps.third_party.commons_codec
implementation deps.third_party.fastscroll
implementation(deps.third_party.jgit) {
@@ -102,7 +105,6 @@ dependencies {
implementation deps.third_party.sshj
implementation deps.third_party.bouncycastle
implementation deps.third_party.plumber
- implementation deps.third_party.openpgp_ktx
implementation deps.third_party.ssh_auth
implementation deps.third_party.timber
implementation deps.third_party.timberkt
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b0c3193dc..2098abc98 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,6 +45,14 @@
+
+
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
index 81a739882..a9b1219d3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
@@ -17,6 +17,8 @@ import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.zxing.integration.android.IntentIntegrator
+import com.google.zxing.integration.android.IntentIntegrator.QR_CODE
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
@@ -62,6 +64,33 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
with(binding) {
setContentView(root)
generatePassword.setOnClickListener { generatePassword() }
+ otpImportButton.setOnClickListener {
+ registerForActivityResult(StartActivityForResult()) { result ->
+ if (result.resultCode == RESULT_OK) {
+ otpImportButton.isVisible = false
+ val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data)
+ val contents = if (intentResult.contents.startsWith("otpauth://")) {
+ "${intentResult.contents}\n"
+ } else {
+ "totp: ${intentResult.contents}\n"
+ }
+ val currentExtras = extraContent.text.toString()
+ if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
+ extraContent.append("\n$contents")
+ else
+ extraContent.append(contents)
+ snackbar(message = getString(R.string.otp_import_success))
+ } else {
+ snackbar(message = getString(R.string.otp_import_failure))
+ }
+ }.launch(
+ IntentIntegrator(this@PasswordCreationActivity)
+ .setOrientationLocked(false)
+ .setBeepEnabled(false)
+ .setDesiredBarcodeFormats(QR_CODE)
+ .createScanIntent()
+ )
+ }
category.apply {
if (suggestedName != null || suggestedPass != null || shouldGeneratePassword) {
@@ -95,7 +124,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
val username = filename.text.toString()
val extras = "username:$username\n${extraContent.text}"
- filename.setText("")
+ filename.text?.clear()
extraContent.setText(extras)
} else {
// User wants to disable username encryption, so we extract the
@@ -104,20 +133,20 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
val username = entry.username
// username should not be null here by the logic in
- // updateEncryptUsernameState, but it could still happen due to
+ // updateViewState, but it could still happen due to
// input lag.
if (username != null) {
filename.setText(username)
extraContent.setText(entry.extraContentWithoutAuthData)
}
}
- updateEncryptUsernameState()
+ updateViewState()
}
}
listOf(filename, extraContent).forEach {
- it.doOnTextChanged { _, _, _, _ -> updateEncryptUsernameState() }
+ it.doOnTextChanged { _, _, _, _ -> updateViewState() }
}
- updateEncryptUsernameState()
+ updateViewState()
}
suggestedPass?.let {
password.setText(it)
@@ -158,17 +187,18 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
}
- private fun updateEncryptUsernameState() = with(binding) {
+ private fun updateViewState() = with(binding) {
+ // Use PasswordEntry to parse extras for username
+ val entry = PasswordEntry("PLACEHOLDER\n${extraContent.text}")
encryptUsername.apply {
if (visibility != View.VISIBLE)
return@with
val hasUsernameInFileName = filename.text.toString().isNotBlank()
- // Use PasswordEntry to parse extras for username
- val entry = PasswordEntry("PLACEHOLDER\n${extraContent.text}")
val hasUsernameInExtras = entry.hasUsername()
isEnabled = hasUsernameInFileName xor hasUsernameInExtras
isChecked = hasUsernameInExtras
}
+ otpImportButton.isVisible = !entry.hasTotp()
}
/**
diff --git a/app/src/main/res/drawable/ic_qr_code_scanner.xml b/app/src/main/res/drawable/ic_qr_code_scanner.xml
new file mode 100644
index 000000000..45a618ac2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_qr_code_scanner.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/password_creation_activity.xml b/app/src/main/res/layout/password_creation_activity.xml
index 13af597c1..e0b25786e 100644
--- a/app/src/main/res/layout/password_creation_activity.xml
+++ b/app/src/main/res/layout/password_creation_activity.xml
@@ -84,6 +84,17 @@
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2f04b3887..3023d9957 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -383,4 +383,7 @@
Failed to write password file to the store, please try again.
Failed to delete password file %1$s from the store, please delete it manually.
File already exists, please use a different name
+ Add OTP
+ Successfully imported TOTP configuration
+ Failed to import TOTP configuration
diff --git a/dependencies.gradle b/dependencies.gradle
index 0246f5689..f5c7c965e 100644
--- a/dependencies.gradle
+++ b/dependencies.gradle
@@ -44,6 +44,11 @@ ext.deps = [
swiperefreshlayout: 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
],
+ first_party: [
+ openpgp_ktx: 'com.github.android-password-store:openpgp-ktx:2.0.0',
+ zxing_android_embedded: 'com.github.android-password-store:zxing-android-embedded:v4.1.0-aps'
+ ],
+
third_party: [
bouncycastle: 'org.bouncycastle:bcprov-jdk15on:1.65.01',
commons_codec: 'commons-codec:commons-codec:1.13',
@@ -52,7 +57,6 @@ ext.deps = [
jgit: 'org.eclipse.jgit:org.eclipse.jgit:3.7.1.201504261725-r',
leakcanary: 'com.squareup.leakcanary:leakcanary-android:2.4',
plumber: 'com.squareup.leakcanary:plumber-android:2.4',
- openpgp_ktx: 'com.github.android-password-store:openpgp-ktx:2.0.0',
sshj: 'com.hierynomus:sshj:0.29.0',
ssh_auth: 'org.sufficientlysecure:sshauthentication-api:1.0',
timber: 'com.jakewharton.timber:timber:4.7.1',