diff options
author | Harsh Shandilya <msfjarvis@gmail.com> | 2020-06-30 19:21:49 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-06-30 19:21:49 +0530 |
commit | 5e74507d5b3f3de8a4f0881fa14f87058e9a63fc (patch) | |
tree | ccab0b45fb642074bbe6691c82a7e2f3eedc0c10 | |
parent | 57f125a4da9f8bad69c2b2e2e6540d245bdd44c4 (diff) |
Allow importing TOTP configuration through QR codes (#903)
Co-authored-by: Fabian Henneke <fabian@henneke.me>
-rw-r--r-- | CHANGELOG.md | 1 | ||||
-rw-r--r-- | app/build.gradle | 4 | ||||
-rw-r--r-- | app/src/main/AndroidManifest.xml | 8 | ||||
-rw-r--r-- | app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt | 46 | ||||
-rw-r--r-- | app/src/main/res/drawable/ic_qr_code_scanner.xml | 9 | ||||
-rw-r--r-- | app/src/main/res/layout/password_creation_activity.xml | 13 | ||||
-rw-r--r-- | app/src/main/res/values/strings.xml | 3 | ||||
-rw-r--r-- | dependencies.gradle | 6 |
8 files changed, 79 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index adcdc57f..a10857c9 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 70b1f1f7..eaef71c2 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 b0c3193d..2098abc9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,6 +46,14 @@ </activity> <activity + android:name="com.journeyapps.barcodescanner.CaptureActivity" + android:clearTaskOnLaunch="true" + android:stateNotNeeded="true" + android:theme="@style/zxing_CaptureTheme" + android:windowSoftInputMode="stateAlwaysHidden" + tools:node="replace" /> + + <activity android:name=".git.GitOperationActivity" android:theme="@style/NoBackgroundTheme" /> 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 81a73988..a9b1219d 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 00000000..45a618ac --- /dev/null +++ b/app/src/main/res/drawable/ic_qr_code_scanner.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M9.5,6.5v3h-3v-3H9.5M11,5H5v6h6V5L11,5zM9.5,14.5v3h-3v-3H9.5M11,13H5v6h6V13L11,13zM17.5,6.5v3h-3v-3H17.5M19,5h-6v6h6V5L19,5zM13,13h1.5v1.5H13V13zM14.5,14.5H16V16h-1.5V14.5zM16,13h1.5v1.5H16V13zM13,16h1.5v1.5H13V16zM14.5,17.5H16V19h-1.5V17.5zM16,16h1.5v1.5H16V16zM17.5,14.5H19V16h-1.5V14.5zM17.5,17.5H19V19h-1.5V17.5zM22,7h-2V4h-3V2h5V7zM22,22v-5h-2v3h-3v2H22zM2,22h5v-2H4v-3H2V22zM2,2v5h2V4h3V2H2z" + android:fillColor="#000000"/> +</vector> diff --git a/app/src/main/res/layout/password_creation_activity.xml b/app/src/main/res/layout/password_creation_activity.xml index 13af597c..e0b25786 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 @@ </com.google.android.material.textfield.TextInputLayout> + <com.google.android.material.button.MaterialButton + android:id="@+id/otp_import_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:text="@string/add_otp" + app:icon="@drawable/ic_qr_code_scanner" + app:iconTint="?attr/colorOnSecondary" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/extra_input_layout" /> + <com.google.android.material.switchmaterial.SwitchMaterial android:id="@+id/encrypt_username" android:layout_width="match_parent" @@ -92,6 +103,6 @@ android:text="@string/crypto_encrypt_username_label" android:visibility="gone" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/extra_input_layout" + app:layout_constraintTop_toBottomOf="@id/otp_import_button" tools:visibility="visible" /> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2f04b388..3023d995 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -383,4 +383,7 @@ <string name="password_creation_file_write_fail_message">Failed to write password file to the store, please try again.</string> <string name="password_creation_file_delete_fail_message">Failed to delete password file %1$s from the store, please delete it manually.</string> <string name="password_creation_duplicate_error">File already exists, please use a different name</string> + <string name="add_otp">Add OTP</string> + <string name="otp_import_success">Successfully imported TOTP configuration</string> + <string name="otp_import_failure">Failed to import TOTP configuration</string> </resources> diff --git a/dependencies.gradle b/dependencies.gradle index 0246f568..f5c7c965 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', |