Skip to content
This repository has been archived by the owner on Oct 15, 2024. It is now read-only.

Reintroduce TOTP support #890

Merged
merged 28 commits into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f7c34b5
Reintroduce TOTP support
msfjarvis Jun 27, 2020
020313f
update changelog
msfjarvis Jun 27, 2020
a6757b7
Merge branch 'develop' into feature/totp
msfjarvis Jun 27, 2020
cd2bfcb
Remove dangling HOTP references
msfjarvis Jun 27, 2020
9609731
Simplify tests
msfjarvis Jun 27, 2020
18570ec
Extract hardcoded string
msfjarvis Jun 27, 2020
2472298
Add tests for TOTP
msfjarvis Jun 27, 2020
5a7c361
Merge branch 'develop' into feature/totp
msfjarvis Jun 27, 2020
e3af34d
PasswordEntry: cleanups
msfjarvis Jun 27, 2020
af95522
Rollback unrelated cleanups
msfjarvis Jun 27, 2020
f219a83
KDoc fixups
msfjarvis Jun 27, 2020
57740fe
Add tests for UriTotpFinder
msfjarvis Jun 27, 2020
b7fecad
Allow viewing OTP codes directly
msfjarvis Jun 28, 2020
cd7289e
Merge branch 'develop' into feature/totp
msfjarvis Jun 28, 2020
f630055
Refactor OTP generation and add tests
fmeum Jun 28, 2020
867fd63
Fix crash if OTP period does not parse as number
fmeum Jun 28, 2020
caf7d60
Improve OTP UI
fmeum Jun 28, 2020
02c0e1e
Fixup imports
msfjarvis Jun 28, 2020
e231498
Drop OTP URLs from extra content
msfjarvis Jun 28, 2020
e28e5c7
Show OTP copied text in Snackbar
msfjarvis Jun 28, 2020
f52bea9
Ignore both TOTP formats
msfjarvis Jun 28, 2020
c86a652
Add testcase for totp: style secrets
msfjarvis Jun 28, 2020
9286295
Add tests and error handling to Base32 decoding
fmeum Jun 28, 2020
18eb6b6
Merge branch 'develop' into feature/totp
msfjarvis Jun 28, 2020
fd61374
Add test for padded vs unpadded invariant of the same secret
msfjarvis Jun 28, 2020
ccac08f
Fix formatting fail
msfjarvis Jun 28, 2020
908d426
Otp: add check for invalid algorithm
msfjarvis Jun 29, 2020
b181d63
Revert "Otp: add check for invalid algorithm"
msfjarvis Jun 29, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file.
- Folder names that were very long did not look right
- Error message for wrong SSH/HTTPS password now looks cleaner

### Added

- TOTP support is reintroduced by popular demand. HOTP continues to be unsupported and heavily discouraged.

## [1.9.1] - 2020-06-28

### Fixed
Expand Down
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ dependencies {
implementation deps.kotlin.coroutines.android
implementation deps.kotlin.coroutines.core

implementation deps.third_party.commons_codec
implementation deps.third_party.fastscroll
implementation(deps.third_party.jgit) {
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/

package com.zeapo.pwdstore.utils

import org.junit.Test
import kotlin.test.assertEquals

class UriTotpFinderTest {

private val totpFinder = UriTotpFinder()

@Test
fun findSecret() {
assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", totpFinder.findSecret(TOTP_URI))
assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", totpFinder.findSecret("name\npassword\ntotp: HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"))
}

@Test
fun findDigits() {
assertEquals("12", totpFinder.findDigits(TOTP_URI))
}

@Test
fun findPeriod() {
assertEquals(25, totpFinder.findPeriod(TOTP_URI))
}

@Test
fun findAlgorithm() {
assertEquals("SHA256", totpFinder.findAlgorithm(TOTP_URI))
}

companion object {
const val TOTP_URI = "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA256&digits=12&period=25"
}
}
78 changes: 0 additions & 78 deletions app/src/main/java/com/zeapo/pwdstore/PasswordEntry.kt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.splitLines
import kotlinx.coroutines.CoroutineScope
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import androidx.annotation.RequiresApi
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.PasswordRepository
import java.io.File
import java.security.MessageDigest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import android.widget.Toast
import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
import com.zeapo.pwdstore.autofill.oreo.Credentials
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
import com.zeapo.pwdstore.autofill.oreo.FillableForm
import com.zeapo.pwdstore.model.PasswordEntry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/com/zeapo/pwdstore/crypto/BasePgpActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import android.view.WindowManager
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.CallSuper
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.tag
Expand Down Expand Up @@ -163,6 +164,7 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou

return DateUtils.getRelativeTimeSpanString(this, timeStamp, true)
}

/**
* Base handling of OpenKeychain errors based on the error contained in [result]. Subclasses
* can use this when they want to default to sane error handling.
Expand Down Expand Up @@ -190,12 +192,16 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
* Copies provided [text] to the clipboard. Shows a [Snackbar] which can be disabled by passing
* [showSnackbar] as false.
*/
fun copyTextToClipboard(text: String?, showSnackbar: Boolean = true) {
fun copyTextToClipboard(
text: String?,
showSnackbar: Boolean = true,
@StringRes snackbarTextRes: Int = R.string.clipboard_copied_text
) {
val clipboard = clipboard ?: return
val clip = ClipData.newPlainText("pgp_handler_result_pm", text)
clipboard.setPrimaryClip(clip)
if (showSnackbar) {
snackbar(message = resources.getString(R.string.clipboard_copied_text))
snackbar(message = resources.getString(snackbarTextRes))
}
}

Expand Down
49 changes: 41 additions & 8 deletions app/src/main/java/com/zeapo/pwdstore/crypto/DecryptActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,23 @@ import androidx.activity.result.contract.ActivityResultContracts.StartActivityFo
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.databinding.DecryptLayoutBinding
import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.Otp
import com.zeapo.pwdstore.utils.viewBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.msfjarvis.openpgpktx.util.OpenPgpApi
import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection
import org.openintents.openpgp.IOpenPgpService2
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.Date
import kotlin.time.ExperimentalTime
import kotlin.time.seconds

class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
private val binding by viewBinding(DecryptLayoutBinding::inflate)
Expand Down Expand Up @@ -125,6 +131,7 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)))
}

@OptIn(ExperimentalTime::class)
private fun decryptAndVerify(receivedIntent: Intent? = null) {
if (api == null) {
bindToOpenKeychain(this, openKeychainResult)
Expand Down Expand Up @@ -163,14 +170,16 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
}

if (entry.hasExtraContent()) {
extraContentContainer.visibility = View.VISIBLE
extraContent.typeface = monoTypeface
extraContent.setText(entry.extraContentWithoutUsername)
if (!showExtraContent) {
extraContent.transformationMethod = PasswordTransformationMethod.getInstance()
if (entry.extraContentWithoutAuthData.isNotEmpty()) {
extraContentContainer.visibility = View.VISIBLE
extraContent.typeface = monoTypeface
extraContent.setText(entry.extraContentWithoutAuthData)
if (!showExtraContent) {
extraContent.transformationMethod = PasswordTransformationMethod.getInstance()
}
extraContentContainer.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutAuthData) }
extraContent.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutAuthData) }
}
extraContentContainer.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) }
extraContent.setOnClickListener { copyTextToClipboard(entry.extraContentWithoutUsername) }

if (entry.hasUsername()) {
usernameText.typeface = monoTypeface
Expand All @@ -180,6 +189,30 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
} else {
usernameTextContainer.visibility = View.GONE
}

if (entry.hasTotp()) {
otpTextContainer.visibility = View.VISIBLE
otpTextContainer.setEndIconOnClickListener {
copyTextToClipboard(
otpText.text.toString(),
snackbarTextRes = R.string.clipboard_otp_copied_text
)
}
launch(Dispatchers.IO) {
repeat(Int.MAX_VALUE) {
val code = Otp.calculateCode(
entry.totpSecret!!,
Date().time / (1000 * entry.totpPeriod),
entry.totpAlgorithm,
entry.digits
) ?: "Error"
withContext(Dispatchers.Main) {
otpText.setText(code)
}
delay(30.seconds)
}
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,16 @@ import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.PasswordEntry
import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
import com.zeapo.pwdstore.databinding.PasswordCreationActivityBinding
import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment
import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.utils.snackbar
import com.zeapo.pwdstore.utils.viewBinding
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -108,7 +108,7 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
// input lag.
if (username != null) {
filename.setText(username)
extraContent.setText(entry.extraContentWithoutUsername)
extraContent.setText(entry.extraContentWithoutAuthData)
}
}
updateEncryptUsernameState()
Expand Down
Loading