all: reformat with Spotless again

Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
This commit is contained in:
Harsh Shandilya 2021-05-15 12:56:49 +05:30
parent 2fcb285e27
commit 7e2eb2425e
No known key found for this signature in database
GPG key ID: 366D7BBAD1031E80
108 changed files with 1385 additions and 473 deletions

View file

@ -2,4 +2,4 @@
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
</component>

View file

@ -3,4 +3,4 @@
<option name="notice" value="Copyright © 2014-&amp;#36;today.year The Android Password Store Authors. All Rights Reserved.&#10;SPDX-License-Identifier: GPL-3.0-only" />
<option name="myName" value="APS" />
</copyright>
</component>
</component>

View file

@ -22,4 +22,4 @@
</GradleProjectSettings>
</option>
</component>
</project>
</project>

View file

@ -37,4 +37,4 @@
<option name="url" value="file:$USER_HOME$/.m2/repository/" />
</remote-repository>
</component>
</project>
</project>

View file

@ -3,4 +3,4 @@
<component name="KotlinScriptingSettings">
<option name="suppressDefinitionsCheck" value="true" />
</component>
</project>
</project>

View file

@ -1,3 +1,3 @@
<component name="DependencyValidationManager">
<scope name="third_party" pattern="src[Android-Password-Store.app]:mozilla.components.lib.publicsuffixlist..*" />
</component>
</component>

View file

@ -6,4 +6,4 @@
<issue id="InvalidPackage">
<ignore regexp="X509LDAPCertStoreSpi" />
</issue>
</lint>
</lint>

View file

@ -115,7 +115,10 @@ class MigrationsTest {
putBoolean(PreferenceKeys.CLEAR_CLIPBOARD_20X, true)
}
runMigrations(context)
assertEquals(true, context.sharedPrefs.getBoolean(PreferenceKeys.CLEAR_CLIPBOARD_HISTORY, false))
assertEquals(
true,
context.sharedPrefs.getBoolean(PreferenceKeys.CLEAR_CLIPBOARD_HISTORY, false)
)
assertFalse(context.sharedPrefs.contains(PreferenceKeys.CLEAR_CLIPBOARD_20X))
}
}

View file

@ -32,7 +32,9 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
override fun onCreate() {
super.onCreate()
instance = this
if (BuildConfig.ENABLE_DEBUG_FEATURES || prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)) {
if (BuildConfig.ENABLE_DEBUG_FEATURES ||
prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)
) {
plant(DebugTree())
StrictMode.setVmPolicy(VmPolicy.Builder().detectAll().penaltyLog().build())
StrictMode.setThreadPolicy(ThreadPolicy.Builder().detectAll().penaltyLog().build())

View file

@ -199,8 +199,11 @@ object PasswordRepository {
fun getFilesList(path: File?): ArrayList<File> {
if (path == null || !path.exists()) return ArrayList()
val directories = (path.listFiles(FileFilter { pathname -> pathname.isDirectory }) ?: emptyArray()).toList()
val files = (path.listFiles(FileFilter { pathname -> pathname.extension == "gpg" }) ?: emptyArray()).toList()
val directories =
(path.listFiles(FileFilter { pathname -> pathname.isDirectory }) ?: emptyArray()).toList()
val files =
(path.listFiles(FileFilter { pathname -> pathname.extension == "gpg" }) ?: emptyArray())
.toList()
val items = ArrayList<File>()
items.addAll(directories)
@ -216,7 +219,11 @@ object PasswordRepository {
* @return a list of password items
*/
@JvmStatic
fun getPasswords(path: File, rootDir: File, sortOrder: PasswordSortOrder): ArrayList<PasswordItem> {
fun getPasswords(
path: File,
rootDir: File,
sortOrder: PasswordSortOrder
): ArrayList<PasswordItem> {
// We need to recover the passwords then parse the files
val passList = getFilesList(path).also { it.sortBy { f -> f.name } }
val passwordList = ArrayList<PasswordItem>()

View file

@ -55,7 +55,8 @@ class FieldItemAdapter(
notifyDataSetChanged()
}
class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) : RecyclerView.ViewHolder(itemView) {
class FieldItemViewHolder(itemView: View, val binding: ItemFieldBinding) :
RecyclerView.ViewHolder(itemView) {
fun bind(fieldItem: FieldItem, showPassword: Boolean, copyTextToClipBoard: (String?) -> Unit) {
with(binding) {
@ -66,7 +67,8 @@ class FieldItemAdapter(
when (fieldItem.action) {
FieldItem.ActionType.COPY -> {
itemTextContainer.apply {
endIconDrawable = ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy)
endIconDrawable =
ContextCompat.getDrawable(itemView.context, R.drawable.ic_content_copy)
endIconMode = TextInputLayout.END_ICON_CUSTOM
setEndIconOnClickListener { copyTextToClipBoard(itemText.text.toString()) }
}

View file

@ -35,7 +35,9 @@ open class PasswordItemRecyclerAdapter :
return super.onItemClicked(listener) as PasswordItemRecyclerAdapter
}
override fun onSelectionChanged(listener: (selection: Selection<String>) -> Unit): PasswordItemRecyclerAdapter {
override fun onSelectionChanged(
listener: (selection: Selection<String>) -> Unit
): PasswordItemRecyclerAdapter {
return super.onSelectionChanged(listener) as PasswordItemRecyclerAdapter
}
@ -59,7 +61,8 @@ open class PasswordItemRecyclerAdapter :
name.text = spannable
if (item.type == PasswordItem.TYPE_CATEGORY) {
folderIndicator.visibility = View.VISIBLE
val count = item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size ?: 0
val count =
item.file.listFiles { path -> path.isDirectory || path.extension == "gpg" }?.size ?: 0
childCount.visibility = if (count > 0) View.VISIBLE else View.GONE
childCount.text = "$count"
} else {
@ -74,7 +77,8 @@ open class PasswordItemRecyclerAdapter :
}
}
class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) : ItemDetailsLookup<String>() {
class PasswordItemDetailsLookup(private val recyclerView: RecyclerView) :
ItemDetailsLookup<String>() {
override fun getItemDetails(event: MotionEvent): ItemDetails<String>? {
val view = recyclerView.findChildViewUnder(event.x, event.y) ?: return null

View file

@ -73,7 +73,12 @@ class AutofillDecryptActivity : AppCompatActivity() {
putExtra(EXTRA_SEARCH_ACTION, false)
putExtra(EXTRA_FILE_PATH, file.absolutePath)
}
return PendingIntent.getActivity(context, decryptFileRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT)
return PendingIntent.getActivity(
context,
decryptFileRequestCode++,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
.intentSender
}
}
@ -124,9 +129,17 @@ class AutofillDecryptActivity : AppCompatActivity() {
setResult(RESULT_CANCELED)
} else {
val fillInDataset =
AutofillResponseBuilder.makeFillInDataset(this@AutofillDecryptActivity, credentials, clientState, action)
AutofillResponseBuilder.makeFillInDataset(
this@AutofillDecryptActivity,
credentials,
clientState,
action
)
withContext(Dispatchers.Main) {
setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) })
setResult(
RESULT_OK,
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
)
}
}
withContext(Dispatchers.Main) { finish() }
@ -137,7 +150,11 @@ class AutofillDecryptActivity : AppCompatActivity() {
super.onDestroy()
}
private suspend fun executeOpenPgpApi(data: Intent, input: InputStream, output: OutputStream): Intent? {
private suspend fun executeOpenPgpApi(
data: Intent,
input: InputStream,
output: OutputStream
): Intent? {
var openPgpServiceConnection: OpenPgpServiceConnection? = null
val openPgpService =
suspendCoroutine<IOpenPgpService2> { cont ->
@ -177,7 +194,9 @@ class AutofillDecryptActivity : AppCompatActivity() {
return null
}
.onSuccess { result ->
return when (val resultCode = result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
return when (val resultCode =
result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)
) {
OpenPgpApi.RESULT_CODE_SUCCESS -> {
runCatching {
val entry =
@ -185,7 +204,12 @@ class AutofillDecryptActivity : AppCompatActivity() {
@Suppress("BlockingMethodInNonBlockingContext")
passwordEntryFactory.create(lifecycleScope, decryptedOutput.toByteArray())
}
AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure)
AutofillPreferences.credentialsFromStoreEntry(
this,
file,
entry,
directoryStructure
)
}
.getOrElse { e ->
e(e) { "Failed to parse password entry" }
@ -193,7 +217,8 @@ class AutofillDecryptActivity : AppCompatActivity() {
}
}
OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
val pendingIntent: PendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!
val pendingIntent: PendingIntent =
result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!!
runCatching {
val intentToResume =
withContext(Dispatchers.Main) {
@ -215,10 +240,16 @@ class AutofillDecryptActivity : AppCompatActivity() {
val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR)
if (error != null) {
withContext(Dispatchers.Main) {
Toast.makeText(applicationContext, "Error from OpenKeyChain: ${error.message}", Toast.LENGTH_LONG)
Toast.makeText(
applicationContext,
"Error from OpenKeyChain: ${error.message}",
Toast.LENGTH_LONG
)
.show()
}
e { "OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}" }
e {
"OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}"
}
}
null
}

View file

@ -46,11 +46,16 @@ class AutofillFilterView : AppCompatActivity() {
private const val HEIGHT_PERCENTAGE = 0.9
private const val WIDTH_PERCENTAGE = 0.75
private const val EXTRA_FORM_ORIGIN_WEB = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_WEB"
private const val EXTRA_FORM_ORIGIN_APP = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP"
private const val EXTRA_FORM_ORIGIN_WEB =
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_WEB"
private const val EXTRA_FORM_ORIGIN_APP =
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP"
private var matchAndDecryptFileRequestCode = 1
fun makeMatchAndDecryptFileIntentSender(context: Context, formOrigin: FormOrigin): IntentSender {
fun makeMatchAndDecryptFileIntentSender(
context: Context,
formOrigin: FormOrigin
): IntentSender {
val intent =
Intent(context, AutofillFilterView::class.java).apply {
when (formOrigin) {
@ -108,7 +113,9 @@ class AutofillFilterView : AppCompatActivity() {
FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!)
}
else -> {
e { "AutofillFilterActivity started without EXTRA_FORM_ORIGIN_WEB or EXTRA_FORM_ORIGIN_APP" }
e {
"AutofillFilterActivity started without EXTRA_FORM_ORIGIN_WEB or EXTRA_FORM_ORIGIN_APP"
}
finish()
return
}
@ -125,7 +132,8 @@ class AutofillFilterView : AppCompatActivity() {
with(binding) {
rvPassword.apply {
adapter =
SearchableRepositoryAdapter(R.layout.oreo_autofill_filter_row, ::PasswordViewHolder) { item ->
SearchableRepositoryAdapter(R.layout.oreo_autofill_filter_row, ::PasswordViewHolder) {
item ->
val file = item.file.relativeTo(item.rootDir)
val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file)
val identifier = directoryStructure.getIdentifierFor(file)
@ -171,10 +179,15 @@ class AutofillFilterView : AppCompatActivity() {
setOnCheckedChangeListener { _, _ -> updateSearch() }
}
shouldMatch.text =
getString(R.string.oreo_autofill_match_with, formOrigin.getPrettyIdentifier(applicationContext))
getString(
R.string.oreo_autofill_match_with,
formOrigin.getPrettyIdentifier(applicationContext)
)
model.searchResult.observe(this@AutofillFilterView) { result ->
val list = result.passwordItems
(rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) { rvPassword.scrollToPosition(0) }
(rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) {
rvPassword.scrollToPosition(0)
}
// Switch RecyclerView out for a "no results" message if the new list is empty and
// the message is not yet shown (and vice versa).
if ((list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) ||
@ -189,16 +202,21 @@ class AutofillFilterView : AppCompatActivity() {
private fun updateSearch() {
model.search(
binding.search.text.toString().trim(),
filterMode = if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy,
filterMode =
if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy,
searchMode = SearchMode.RecursivelyInSubdirectories,
listMode = ListMode.FilesOnly
)
}
private fun decryptAndFill(item: PasswordItem) {
if (binding.shouldClear.isChecked) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin)
if (binding.shouldMatch.isChecked) AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file)
if (binding.shouldClear.isChecked)
AutofillMatcher.clearMatchesFor(applicationContext, formOrigin)
if (binding.shouldMatch.isChecked)
AutofillMatcher.addMatchFor(applicationContext, formOrigin, item.file)
// intent?.extras? is checked to be non-null in onCreate
decryptAction.launch(AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this))
decryptAction.launch(
AutofillDecryptActivity.makeDecryptFileIntent(item.file, intent!!.extras!!, this)
)
}
}

View file

@ -83,9 +83,15 @@ class AutofillPublisherChangedActivity : AppCompatActivity() {
resetButton.visibility = View.VISIBLE
}
resetButton.setOnClickListener {
AutofillMatcher.clearMatchesFor(this@AutofillPublisherChangedActivity, FormOrigin.App(appPackage))
AutofillMatcher.clearMatchesFor(
this@AutofillPublisherChangedActivity,
FormOrigin.App(appPackage)
)
val fillResponse = intent.getParcelableExtra<FillResponse>(EXTRA_FILL_RESPONSE_AFTER_RESET)
setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) })
setResult(
RESULT_OK,
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) }
)
finish()
}
}
@ -96,13 +102,18 @@ class AutofillPublisherChangedActivity : AppCompatActivity() {
with(binding) {
val packageInfo = packageManager.getPackageInfo(appPackage, PackageManager.GET_META_DATA)
val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime)
warningAppInstallDate.text = getString(R.string.oreo_autofill_warning_publisher_install_time, installTime)
warningAppInstallDate.text =
getString(R.string.oreo_autofill_warning_publisher_install_time, installTime)
val appInfo = packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA)
warningAppName.text = "${packageManager.getApplicationLabel(appInfo)}"
val currentHash = computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage)
warningAppAdvancedInfo.text =
getString(R.string.oreo_autofill_warning_publisher_advanced_info_template, appPackage, currentHash)
getString(
R.string.oreo_autofill_warning_publisher_advanced_info_template,
appPackage,
currentHash
)
}
}
.onFailure { e ->

View file

@ -34,13 +34,20 @@ class AutofillSaveActivity : AppCompatActivity() {
private const val EXTRA_FOLDER_NAME = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_FOLDER_NAME"
private const val EXTRA_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_PASSWORD"
private const val EXTRA_NAME = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_NAME"
private const val EXTRA_SHOULD_MATCH_APP = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP"
private const val EXTRA_SHOULD_MATCH_WEB = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB"
private const val EXTRA_GENERATE_PASSWORD = "dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD"
private const val EXTRA_SHOULD_MATCH_APP =
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP"
private const val EXTRA_SHOULD_MATCH_WEB =
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB"
private const val EXTRA_GENERATE_PASSWORD =
"dev.msfjarvis.aps.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD"
private var saveRequestCode = 1
fun makeSaveIntentSender(context: Context, credentials: Credentials?, formOrigin: FormOrigin): IntentSender {
fun makeSaveIntentSender(
context: Context,
credentials: Credentials?,
formOrigin: FormOrigin
): IntentSender {
val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false)
// Prevent directory traversals
val sanitizedIdentifier =
@ -52,7 +59,11 @@ class AutofillSaveActivity : AppCompatActivity() {
sanitizedIdentifier = sanitizedIdentifier,
username = credentials?.username
)
val fileName = directoryStructure.getSaveFileName(username = credentials?.username, identifier = identifier)
val fileName =
directoryStructure.getSaveFileName(
username = credentials?.username,
identifier = identifier
)
val intent =
Intent(context, AutofillSaveActivity::class.java).apply {
putExtras(
@ -60,13 +71,20 @@ class AutofillSaveActivity : AppCompatActivity() {
EXTRA_FOLDER_NAME to folderName,
EXTRA_NAME to fileName,
EXTRA_PASSWORD to credentials?.password,
EXTRA_SHOULD_MATCH_APP to formOrigin.identifier.takeIf { formOrigin is FormOrigin.App },
EXTRA_SHOULD_MATCH_WEB to formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web },
EXTRA_SHOULD_MATCH_APP to
formOrigin.identifier.takeIf { formOrigin is FormOrigin.App },
EXTRA_SHOULD_MATCH_WEB to
formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web },
EXTRA_GENERATE_PASSWORD to (credentials == null)
)
)
}
return PendingIntent.getActivity(context, saveRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT)
return PendingIntent.getActivity(
context,
saveRequestCode++,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
.intentSender
}
}
@ -94,7 +112,8 @@ class AutofillSaveActivity : AppCompatActivity() {
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath,
PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME),
PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD),
PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
)
)
}
@ -117,8 +136,15 @@ class AutofillSaveActivity : AppCompatActivity() {
}
val credentials = Credentials(username, password, null)
val fillInDataset =
AutofillResponseBuilder.makeFillInDataset(this, credentials, clientState, AutofillAction.Generate)
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
AutofillResponseBuilder.makeFillInDataset(
this,
credentials,
clientState,
AutofillAction.Generate
)
Intent().apply {
putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset)
}
} else {
// Password was extracted from a form, there is nothing to fill.
Intent()

View file

@ -157,7 +157,10 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
return
} else {
previousListener = null
serviceConnection = OpenPgpServiceConnection(this, OPENPGP_PROVIDER, onBoundListener).also { it.bindToService() }
serviceConnection =
OpenPgpServiceConnection(this, OPENPGP_PROVIDER, onBoundListener).also {
it.bindToService()
}
}
}
@ -250,7 +253,10 @@ open class BasePgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBou
fun getParentPath(fullPath: String, repositoryPath: String): String {
val relativePath = getRelativePath(fullPath, repositoryPath)
val index = relativePath.lastIndexOf("/")
return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace("/+".toRegex(), "/")
return "/${relativePath.substring(startIndex = 0, endIndex = index + 1)}/".replace(
"/+".toRegex(),
"/"
)
}
/** /path/to/store/social/facebook.gpg -> social/facebook */

View file

@ -44,7 +44,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
private val binding by viewBinding(DecryptLayoutBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory
private val relativeParentPath by lazy(LazyThreadSafetyMode.NONE) { getParentPath(fullPath, repoPath) }
private val relativeParentPath by lazy(LazyThreadSafetyMode.NONE) {
getParentPath(fullPath, repoPath)
}
private var passwordEntry: PasswordEntry? = null
private val userInteractionRequiredResult =
@ -136,7 +138,10 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
intent.putExtra("REPO_PATH", repoPath)
intent.putExtra(PasswordCreationActivity.EXTRA_FILE_NAME, name)
intent.putExtra(PasswordCreationActivity.EXTRA_PASSWORD, passwordEntry?.password)
intent.putExtra(PasswordCreationActivity.EXTRA_EXTRA_CONTENT, passwordEntry?.extraContentWithoutAuthData)
intent.putExtra(
PasswordCreationActivity.EXTRA_EXTRA_CONTENT,
passwordEntry?.extraContentWithoutAuthData
)
intent.putExtra(PasswordCreationActivity.EXTRA_EDITING, true)
startActivity(intent)
finish()
@ -150,7 +155,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
type = "text/plain"
}
// Always show a picker to give the user a chance to cancel
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to)))
startActivity(
Intent.createChooser(sendIntent, resources.getText(R.string.send_plaintext_password_to))
)
}
@OptIn(ExperimentalTime::class)
@ -166,7 +173,10 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
val outputStream = ByteArrayOutputStream()
lifecycleScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO) { checkNotNull(api).executeApi(data, inputStream, outputStream) }
val result =
withContext(Dispatchers.IO) {
checkNotNull(api).executeApi(data, inputStream, outputStream)
}
when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
OpenPgpApi.RESULT_CODE_SUCCESS -> {
startAutoDismissTimer()
@ -174,7 +184,8 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
val showPassword = settings.getBoolean(PreferenceKeys.SHOW_PASSWORD, true)
val entry = passwordEntryFactory.create(lifecycleScope, outputStream.toByteArray())
val items = arrayListOf<FieldItem>()
val adapter = FieldItemAdapter(emptyList(), showPassword) { text -> copyTextToClipboard(text) }
val adapter =
FieldItemAdapter(emptyList(), showPassword) { text -> copyTextToClipboard(text) }
if (settings.getBoolean(PreferenceKeys.COPY_ON_DECRYPT, false)) {
copyPasswordToClipboard(entry.password)
@ -190,7 +201,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
if (entry.hasTotp()) {
launch {
items.add(FieldItem.createOtpField(entry.totp.value))
entry.totp.collect { code -> withContext(Dispatchers.Main) { adapter.updateOTPCode(code) } }
entry.totp.collect { code ->
withContext(Dispatchers.Main) { adapter.updateOTPCode(code) }
}
}
}
@ -198,7 +211,9 @@ class DecryptActivity : BasePgpActivity(), OpenPgpServiceConnection.OnBound {
items.add(FieldItem.createUsernameField(entry.username!!))
}
entry.extraContent.forEach { (key, value) -> items.add(FieldItem(key, value, FieldItem.ActionType.COPY)) }
entry.extraContent.forEach { (key, value) ->
items.add(FieldItem(key, value, FieldItem.ActionType.COPY))
}
binding.recyclerView.adapter = adapter
adapter.updateItems(items)

View file

@ -55,7 +55,9 @@ class GetKeyIdsActivity : BasePgpActivity() {
OpenPgpApi.RESULT_CODE_SUCCESS -> {
runCatching {
val ids =
result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map { OpenPgpUtils.convertKeyIdToHex(it) }
result.getLongArrayExtra(OpenPgpApi.RESULT_KEY_IDS)?.map {
OpenPgpUtils.convertKeyIdToHex(it)
}
?: emptyList()
val keyResult = Intent().putExtra(OpenPgpApi.EXTRA_KEY_IDS, ids.toTypedArray())
setResult(RESULT_OK, keyResult)

View file

@ -63,14 +63,24 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
private val binding by viewBinding(PasswordCreationActivityBinding::inflate)
@Inject lateinit var passwordEntryFactory: PasswordEntryFactory
private val suggestedName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) }
private val suggestedPass by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_PASSWORD) }
private val suggestedExtra by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_EXTRA_CONTENT) }
private val suggestedName by lazy(LazyThreadSafetyMode.NONE) {
intent.getStringExtra(EXTRA_FILE_NAME)
}
private val suggestedPass by lazy(LazyThreadSafetyMode.NONE) {
intent.getStringExtra(EXTRA_PASSWORD)
}
private val suggestedExtra by lazy(LazyThreadSafetyMode.NONE) {
intent.getStringExtra(EXTRA_EXTRA_CONTENT)
}
private val shouldGeneratePassword by lazy(LazyThreadSafetyMode.NONE) {
intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
}
private val editing by lazy(LazyThreadSafetyMode.NONE) { intent.getBooleanExtra(EXTRA_EDITING, false) }
private val oldFileName by lazy(LazyThreadSafetyMode.NONE) { intent.getStringExtra(EXTRA_FILE_NAME) }
private val editing by lazy(LazyThreadSafetyMode.NONE) {
intent.getBooleanExtra(EXTRA_EDITING, false)
}
private val oldFileName by lazy(LazyThreadSafetyMode.NONE) {
intent.getStringExtra(EXTRA_FILE_NAME)
}
private var oldCategory: String? = null
private var copy: Boolean = false
private var encryptionIntent: Intent = Intent()
@ -99,7 +109,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
val intentResult = IntentIntegrator.parseActivityResult(RESULT_OK, result.data)
val contents = "${intentResult.contents}\n"
val currentExtras = binding.extraContent.text.toString()
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') binding.extraContent.append("\n$contents")
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
binding.extraContent.append("\n$contents")
else binding.extraContent.append(contents)
snackbar(message = getString(R.string.otp_import_success))
} else {
@ -113,18 +124,27 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
result.data?.getStringArrayExtra(OpenPgpApi.EXTRA_KEY_IDS)?.let { keyIds ->
lifecycleScope.launch {
val gpgIdentifierFile = File(PasswordRepository.getRepositoryDirectory(), ".gpg-id")
withContext(Dispatchers.IO) { gpgIdentifierFile.writeText((keyIds + "").joinToString("\n")) }
withContext(Dispatchers.IO) {
gpgIdentifierFile.writeText((keyIds + "").joinToString("\n"))
}
commitChange(
getString(
R.string.git_commit_gpg_id,
getLongName(gpgIdentifierFile.parentFile!!.absolutePath, repoPath, gpgIdentifierFile.name)
getLongName(
gpgIdentifierFile.parentFile!!.absolutePath,
repoPath,
gpgIdentifierFile.name
)
)
)
.onSuccess { encrypt(encryptionIntent) }
}
}
} else {
snackbar(message = getString(R.string.gpg_key_select_mandatory), length = Snackbar.LENGTH_LONG)
snackbar(
message = getString(R.string.gpg_key_select_mandatory),
length = Snackbar.LENGTH_LONG
)
}
}
@ -148,24 +168,31 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
super.onCreate(savedInstanceState)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
bindToOpenKeychain(this)
title = if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title)
title =
if (editing) getString(R.string.edit_password) else getString(R.string.new_password_title)
with(binding) {
setContentView(root)
generatePassword.setOnClickListener { generatePassword() }
otpImportButton.setOnClickListener {
supportFragmentManager.setFragmentResultListener(OTP_RESULT_REQUEST_KEY, this@PasswordCreationActivity) {
requestKey,
bundle ->
supportFragmentManager.setFragmentResultListener(
OTP_RESULT_REQUEST_KEY,
this@PasswordCreationActivity
) { requestKey, bundle ->
if (requestKey == OTP_RESULT_REQUEST_KEY) {
val contents = bundle.getString(RESULT)
val currentExtras = binding.extraContent.text.toString()
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n') binding.extraContent.append("\n$contents")
if (currentExtras.isNotEmpty() && currentExtras.last() != '\n')
binding.extraContent.append("\n$contents")
else binding.extraContent.append(contents)
}
}
val hasCamera = packageManager?.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) == true
if (hasCamera) {
val items = arrayOf(getString(R.string.otp_import_qr_code), getString(R.string.otp_import_manual_entry))
val items =
arrayOf(
getString(R.string.otp_import_qr_code),
getString(R.string.otp_import_manual_entry)
)
MaterialAlertDialogBuilder(this@PasswordCreationActivity)
.setItems(items) { _, index ->
when (index) {
@ -209,7 +236,8 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
// in the encrypted extras. This only makes sense if the directory structure is
// FileBased.
if (suggestedName == null &&
AutofillPreferences.directoryStructure(this@PasswordCreationActivity) == DirectoryStructure.FileBased
AutofillPreferences.directoryStructure(this@PasswordCreationActivity) ==
DirectoryStructure.FileBased
) {
encryptUsername.apply {
visibility = View.VISIBLE
@ -226,7 +254,10 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
// User wants to disable username encryption, so we extract the
// username from the encrypted extras and use it as the filename.
val entry =
passwordEntryFactory.create(lifecycleScope, "PASSWORD\n${extraContent.text}".encodeToByteArray())
passwordEntryFactory.create(
lifecycleScope,
"PASSWORD\n${extraContent.text}".encodeToByteArray()
)
val username = entry.username
// username should not be null here by the logic in
@ -239,7 +270,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
}
}
listOf(filename, extraContent).forEach { it.doOnTextChanged { _, _, _, _ -> updateViewState() } }
listOf(filename, extraContent).forEach {
it.doOnTextChanged { _, _, _, _ -> updateViewState() }
}
}
suggestedPass?.let {
password.setText(it)
@ -279,21 +312,29 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
private fun generatePassword() {
supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) { requestKey, bundle ->
supportFragmentManager.setFragmentResultListener(PASSWORD_RESULT_REQUEST_KEY, this) {
requestKey,
bundle ->
if (requestKey == PASSWORD_RESULT_REQUEST_KEY) {
binding.password.setText(bundle.getString(RESULT))
}
}
when (settings.getString(PreferenceKeys.PREF_KEY_PWGEN_TYPE) ?: KEY_PWGEN_TYPE_CLASSIC) {
KEY_PWGEN_TYPE_CLASSIC -> PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
KEY_PWGEN_TYPE_XKPASSWD -> XkPasswordGeneratorDialogFragment().show(supportFragmentManager, "xkpwgenerator")
KEY_PWGEN_TYPE_CLASSIC ->
PasswordGeneratorDialogFragment().show(supportFragmentManager, "generator")
KEY_PWGEN_TYPE_XKPASSWD ->
XkPasswordGeneratorDialogFragment().show(supportFragmentManager, "xkpwgenerator")
}
}
private fun updateViewState() =
with(binding) {
// Use PasswordEntry to parse extras for username
val entry = passwordEntryFactory.create(lifecycleScope, "PLACEHOLDER\n${extraContent.text}".encodeToByteArray())
val entry =
passwordEntryFactory.create(
lifecycleScope,
"PLACEHOLDER\n${extraContent.text}".encodeToByteArray()
)
encryptUsername.apply {
if (visibility != View.VISIBLE) return@apply
val hasUsernameInFileName = filename.text.toString().isNotBlank()
@ -354,14 +395,18 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
}
if (gpgIdentifiers.isEmpty()) {
gpgKeySelectAction.launch(Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java))
gpgKeySelectAction.launch(
Intent(this@PasswordCreationActivity, GetKeyIdsActivity::class.java)
)
return@with
}
val keyIds = gpgIdentifiers.filterIsInstance<GpgIdentifier.KeyId>().map { it.id }.toLongArray()
val keyIds =
gpgIdentifiers.filterIsInstance<GpgIdentifier.KeyId>().map { it.id }.toLongArray()
if (keyIds.isNotEmpty()) {
encryptionIntent.putExtra(OpenPgpApi.EXTRA_KEY_IDS, keyIds)
}
val userIds = gpgIdentifiers.filterIsInstance<GpgIdentifier.UserId>().map { it.email }.toTypedArray()
val userIds =
gpgIdentifiers.filterIsInstance<GpgIdentifier.UserId>().map { it.email }.toTypedArray()
if (userIds.isNotEmpty()) {
encryptionIntent.putExtra(OpenPgpApi.EXTRA_USER_IDS, userIds)
}
@ -396,7 +441,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
lifecycleScope.launch(Dispatchers.Main) {
val result =
withContext(Dispatchers.IO) { checkNotNull(api).executeApi(encryptionIntent, inputStream, outputStream) }
withContext(Dispatchers.IO) {
checkNotNull(api).executeApi(encryptionIntent, inputStream, outputStream)
}
when (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
OpenPgpApi.RESULT_CODE_SUCCESS -> {
runCatching {
@ -405,7 +452,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
// Additionally, if we were editing and the incoming and outgoing
// filenames differ, it means we renamed. Ensure that the target
// doesn't already exist to prevent an accidental overwrite.
if ((!editing || (editing && suggestedName != file.nameWithoutExtension)) && file.exists()) {
if ((!editing || (editing && suggestedName != file.nameWithoutExtension)) &&
file.exists()
) {
snackbar(message = getString(R.string.password_creation_duplicate_error))
return@runCatching
}
@ -415,7 +464,9 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
return@runCatching
}
withContext(Dispatchers.IO) { file.outputStream().use { it.write(outputStream.toByteArray()) } }
withContext(Dispatchers.IO) {
file.outputStream().use { it.write(outputStream.toByteArray()) }
}
// associate the new password name with the last name's timestamp in
// history
@ -432,7 +483,10 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
val returnIntent = Intent()
returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path)
returnIntent.putExtra(RETURN_EXTRA_NAME, editName)
returnIntent.putExtra(RETURN_EXTRA_LONG_NAME, getLongName(fullPath, repoPath, editName))
returnIntent.putExtra(
RETURN_EXTRA_LONG_NAME,
getLongName(fullPath, repoPath, editName)
)
if (shouldGeneratePassword) {
val directoryStructure = AutofillPreferences.directoryStructure(applicationContext)
@ -442,13 +496,18 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
returnIntent.putExtra(RETURN_EXTRA_USERNAME, username)
}
if (directoryInputLayout.isVisible && directoryInputLayout.isEnabled && oldFileName != null) {
if (directoryInputLayout.isVisible &&
directoryInputLayout.isEnabled &&
oldFileName != null
) {
val oldFile = File("$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg")
if (oldFile.path != file.path && !oldFile.delete()) {
setResult(RESULT_CANCELED)
MaterialAlertDialogBuilder(this@PasswordCreationActivity)
.setTitle(R.string.password_creation_file_fail_title)
.setMessage(getString(R.string.password_creation_file_delete_fail_message, oldFileName))
.setMessage(
getString(R.string.password_creation_file_delete_fail_message, oldFileName)
)
.setCancelable(false)
.setPositiveButton(android.R.string.ok) { _, _ -> finish() }
.show()
@ -456,9 +515,12 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
}
}
val commitMessageRes = if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text
val commitMessageRes =
if (editing) R.string.git_commit_edit_text else R.string.git_commit_add_text
lifecycleScope.launch {
commitChange(resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName)))
commitChange(
resources.getString(commitMessageRes, getLongName(fullPath, repoPath, editName))
)
.onSuccess {
setResult(RESULT_OK, returnIntent)
finish()

View file

@ -52,7 +52,11 @@ private constructor(
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if (savedInstanceState != null) dismiss()
return layoutInflater.inflate(R.layout.basic_bottom_sheet, container, false)
}
@ -85,7 +89,9 @@ private constructor(
}
if (negativeButtonClickListener != null) {
binding.bottomSheetCancelButton.isVisible = true
negativeButtonLabel?.let { buttonLbl -> binding.bottomSheetCancelButton.text = buttonLbl }
negativeButtonLabel?.let { buttonLbl ->
binding.bottomSheetCancelButton.text = buttonLbl
}
binding.bottomSheetCancelButton.setOnClickListener {
negativeButtonClickListener.onClick(it)
dismiss()
@ -95,7 +101,9 @@ private constructor(
}
)
val gradientDrawable =
GradientDrawable().apply { setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) }
GradientDrawable().apply {
setColor(requireContext().resolveAttribute(android.R.attr.windowBackground))
}
view.background = gradientDrawable
}
@ -133,13 +141,19 @@ private constructor(
return this
}
fun setPositiveButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder {
fun setPositiveButtonClickListener(
buttonLabel: String? = null,
listener: View.OnClickListener
): Builder {
this.positiveButtonClickListener = listener
this.positiveButtonLabel = buttonLabel
return this
}
fun setNegativeButtonClickListener(buttonLabel: String? = null, listener: View.OnClickListener): Builder {
fun setNegativeButtonClickListener(
buttonLabel: String? = null,
listener: View.OnClickListener
): Builder {
this.negativeButtonClickListener = listener
this.negativeButtonLabel = buttonLabel
return this

View file

@ -37,7 +37,11 @@ class ItemCreationBottomSheet : BottomSheetDialogFragment() {
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if (savedInstanceState != null) dismiss()
return inflater.inflate(R.layout.item_create_sheet, container, false)
}
@ -67,7 +71,9 @@ class ItemCreationBottomSheet : BottomSheetDialogFragment() {
}
)
val gradientDrawable =
GradientDrawable().apply { setColor(requireContext().resolveAttribute(android.R.attr.windowBackground)) }
GradientDrawable().apply {
setColor(requireContext().resolveAttribute(android.R.attr.windowBackground))
}
view.background = gradientDrawable
}

View file

@ -36,7 +36,10 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
val callingActivity = requireActivity()
val binding = FragmentPwgenBinding.inflate(layoutInflater)
val monoTypeface = Typeface.createFromAsset(callingActivity.assets, "fonts/sourcecodepro.ttf")
val prefs = requireActivity().applicationContext.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
val prefs =
requireActivity()
.applicationContext
.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE)
builder.setView(binding.root)
@ -65,7 +68,9 @@ class PasswordGeneratorDialogFragment : DialogFragment() {
.apply {
setOnShowListener {
generate(binding.passwordText)
getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener { generate(binding.passwordText) }
getButton(AlertDialog.BUTTON_NEGATIVE).setOnClickListener {
generate(binding.passwordText)
}
}
}
}

View file

@ -49,7 +49,9 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
binding.xkNumWords.setText(prefs.getString(PREF_KEY_NUM_WORDS, DEFAULT_NUMBER_OF_WORDS))
binding.xkSeparator.setText(prefs.getString(PREF_KEY_SEPARATOR, DEFAULT_WORD_SEPARATOR))
binding.xkNumberSymbolMask.setText(prefs.getString(PREF_KEY_EXTRA_SYMBOLS_MASK, DEFAULT_EXTRA_SYMBOLS_MASK))
binding.xkNumberSymbolMask.setText(
prefs.getString(PREF_KEY_EXTRA_SYMBOLS_MASK, DEFAULT_EXTRA_SYMBOLS_MASK)
)
binding.xkPasswordText.typeface = monoTypeface
@ -85,8 +87,12 @@ class XkPasswordGeneratorDialogFragment : DialogFragment() {
.setMinimumWordLength(DEFAULT_MIN_WORD_LENGTH)
.setMaximumWordLength(DEFAULT_MAX_WORD_LENGTH)
.setSeparator(binding.xkSeparator.text.toString())
.appendNumbers(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_DIGIT })
.appendSymbols(binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL })
.appendNumbers(
binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_DIGIT }
)
.appendSymbols(
binding.xkNumberSymbolMask.text!!.count { c -> c == EXTRA_CHAR_PLACEHOLDER_SYMBOL }
)
.setCapitalization(CapsType.valueOf(binding.xkCapType.selectedItem.toString()))
.create()
.fold(

View file

@ -24,7 +24,10 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
passwordList = SelectFolderFragment()
val args = Bundle()
args.putString(PasswordStore.REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath)
args.putString(
PasswordStore.REQUEST_ARG_PATH,
PasswordRepository.getRepositoryDirectory().absolutePath
)
passwordList.arguments = args
@ -32,7 +35,9 @@ class SelectFolderActivity : AppCompatActivity(R.layout.select_folder_layout) {
supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
supportFragmentManager.commit { replace(R.id.pgp_handler_linearlayout, passwordList, PASSWORD_FRAGMENT_TAG) }
supportFragmentManager.commit {
replace(R.id.pgp_handler_linearlayout, passwordList, PASSWORD_FRAGMENT_TAG)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {

View file

@ -35,7 +35,10 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.fab.hide()
recyclerAdapter = PasswordItemRecyclerAdapter().onItemClicked { _, item -> listener.onFragmentInteraction(item) }
recyclerAdapter =
PasswordItemRecyclerAdapter().onItemClicked { _, item ->
listener.onFragmentInteraction(item)
}
binding.passRecycler.apply {
layoutManager = LinearLayoutManager(requireContext())
itemAnimator = null
@ -47,7 +50,9 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH))
model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false)
model.searchResult.observe(viewLifecycleOwner) { result -> recyclerAdapter.submitList(result.passwordItems) }
model.searchResult.observe(viewLifecycleOwner) { result ->
recyclerAdapter.submitList(result.passwordItems)
}
}
override fun onAttach(context: Context) {
@ -58,12 +63,16 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
override fun onFragmentInteraction(item: PasswordItem) {
if (item.type == PasswordItem.TYPE_CATEGORY) {
model.navigateTo(item.file, listMode = ListMode.DirectoriesOnly)
(requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(true)
(requireActivity() as AppCompatActivity).supportActionBar?.setDisplayHomeAsUpEnabled(
true
)
}
}
}
}
.onFailure { throw ClassCastException("$context must implement OnFragmentInteractionListener") }
.onFailure {
throw ClassCastException("$context must implement OnFragmentInteractionListener")
}
}
val currentDir: File

View file

@ -116,7 +116,8 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
"Your local repository appears to be an incomplete Git clone, please delete and re-clone from settings"
)
}
err is TransportException && err.disconnectReason == DisconnectReason.HOST_KEY_NOT_VERIFIABLE -> {
err is TransportException &&
err.disconnectReason == DisconnectReason.HOST_KEY_NOT_VERIFIABLE -> {
SSHException(
DisconnectReason.HOST_KEY_NOT_VERIFIABLE,
"WARNING: The remote host key has changed. If this is expected, please go to Git server settings and clear the saved host key."
@ -135,7 +136,9 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
private fun isExplicitlyUserInitiatedError(throwable: Throwable): Boolean {
var cause: Throwable? = throwable
while (cause != null) {
if (cause is SSHException && cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER) return true
if (cause is SSHException && cause.disconnectReason == DisconnectReason.AUTH_CANCELLED_BY_USER
)
return true
cause = cause.cause
}
return false
@ -154,7 +157,8 @@ abstract class BaseGitActivity : ContinuationContainerActivity() {
while ((rootCause is org.eclipse.jgit.errors.TransportException ||
rootCause is org.eclipse.jgit.api.errors.TransportException ||
rootCause is org.eclipse.jgit.api.errors.InvalidRemoteException ||
(rootCause is UserAuthException && rootCause.message == "Exhausted available authentication methods"))) {
(rootCause is UserAuthException &&
rootCause.message == "Exhausted available authentication methods"))) {
rootCause = rootCause.cause ?: break
}
return rootCause

View file

@ -55,7 +55,12 @@ class GitConfigActivity : BaseGitActivity() {
} else {
GitSettings.authorEmail = email
GitSettings.authorName = name
Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.git_server_config_save_success),
Snackbar.LENGTH_SHORT
)
.show()
Handler(Looper.getMainLooper()).postDelayed(500) { finish() }
}
}
@ -77,7 +82,8 @@ class GitConfigActivity : BaseGitActivity() {
if (repo != null) {
binding.gitHeadStatus.text = headStatusMsg(repo)
// enable the abort button only if we're rebasing or merging
val needsAbort = repo.repositoryState.isRebasing || repo.repositoryState == RepositoryState.MERGING
val needsAbort =
repo.repositoryState.isRebasing || repo.repositoryState == RepositoryState.MERGING
binding.gitAbortRebase.isEnabled = needsAbort
binding.gitAbortRebase.alpha = if (needsAbort) 1.0f else 0.5f
}

View file

@ -89,7 +89,12 @@ class GitServerConfigActivity : BaseGitActivity() {
binding.clearHostKeyButton.isVisible = GitSettings.hasSavedHostKey()
binding.clearHostKeyButton.setOnClickListener {
GitSettings.clearSavedHostKey()
Snackbar.make(binding.root, getString(R.string.clear_saved_host_key_success), Snackbar.LENGTH_LONG).show()
Snackbar.make(
binding.root,
getString(R.string.clear_saved_host_key_success),
Snackbar.LENGTH_LONG
)
.show()
it.isVisible = false
}
binding.saveButton.setOnClickListener {
@ -102,7 +107,9 @@ class GitServerConfigActivity : BaseGitActivity() {
BasicBottomSheet.Builder(this)
.setTitleRes(R.string.https_scheme_with_port_title)
.setMessageRes(R.string.https_scheme_with_port_message)
.setPositiveButtonClickListener { binding.serverUrl.setText(newUrl.replace(PORT_REGEX, "/")) }
.setPositiveButtonClickListener {
binding.serverUrl.setText(newUrl.replace(PORT_REGEX, "/"))
}
.build()
.show(supportFragmentManager, "SSH_SCHEME_WARNING")
return@setOnClickListener
@ -110,7 +117,9 @@ class GitServerConfigActivity : BaseGitActivity() {
BasicBottomSheet.Builder(this)
.setTitleRes(R.string.ssh_scheme_needed_title)
.setMessageRes(R.string.ssh_scheme_needed_message)
.setPositiveButtonClickListener { @Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl") }
.setPositiveButtonClickListener {
@Suppress("SetTextI18n") binding.serverUrl.setText("ssh://$newUrl")
}
.build()
.show(supportFragmentManager, "SSH_SCHEME_WARNING")
return@setOnClickListener
@ -133,7 +142,12 @@ class GitServerConfigActivity : BaseGitActivity() {
)
) {
GitSettings.UpdateConnectionSettingsResult.FailedToParseUrl -> {
Snackbar.make(binding.root, getString(R.string.git_server_config_save_error), Snackbar.LENGTH_LONG).show()
Snackbar.make(
binding.root,
getString(R.string.git_server_config_save_error),
Snackbar.LENGTH_LONG
)
.show()
}
is GitSettings.UpdateConnectionSettingsResult.MissingUsername -> {
when (updateResult.newProtocol) {
@ -154,9 +168,14 @@ class GitServerConfigActivity : BaseGitActivity() {
}
}
GitSettings.UpdateConnectionSettingsResult.Valid -> {
if (isClone && PasswordRepository.getRepository(null) == null) PasswordRepository.initialize()
if (isClone && PasswordRepository.getRepository(null) == null)
PasswordRepository.initialize()
if (!isClone) {
Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT)
Snackbar.make(
binding.root,
getString(R.string.git_server_config_save_success),
Snackbar.LENGTH_SHORT
)
.show()
Handler(Looper.getMainLooper()).postDelayed(500) { finish() }
} else {
@ -206,7 +225,9 @@ class GitServerConfigActivity : BaseGitActivity() {
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory())
val localDirFiles = localDir.listFiles() ?: emptyArray()
// Warn if non-empty folder unless it's a just-initialized store that has just a .git folder
if (localDir.exists() && localDirFiles.isNotEmpty() && !(localDirFiles.size == 1 && localDirFiles[0].name == ".git")
if (localDir.exists() &&
localDirFiles.isNotEmpty() &&
!(localDirFiles.size == 1 && localDirFiles[0].name == ".git")
) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_delete_title)
@ -269,7 +290,9 @@ class GitServerConfigActivity : BaseGitActivity() {
private val PORT_REGEX = ":[0-9]{1,5}/".toRegex()
fun createCloneIntent(context: Context): Intent {
return Intent(context, GitServerConfigActivity::class.java).apply { putExtra("cloning", true) }
return Intent(context, GitServerConfigActivity::class.java).apply {
putExtra("cloning", true)
}
}
}
}

View file

@ -45,7 +45,8 @@ class GitLogAdapter : RecyclerView.Adapter<GitLogAdapter.ViewHolder>() {
override fun getItemCount() = model.size
class ViewHolder(private val binding: GitLogRowLayoutBinding) : RecyclerView.ViewHolder(binding.root) {
class ViewHolder(private val binding: GitLogRowLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(commit: GitCommit) =
with(binding) {

View file

@ -24,7 +24,9 @@ class CloneFragment : Fragment(R.layout.fragment_clone) {
private val binding by viewBinding(FragmentCloneBinding::bind)
private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs }
private val settings by lazy(LazyThreadSafetyMode.NONE) {
requireActivity().applicationContext.sharedPrefs
}
private val cloneAction =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->

View file

@ -32,7 +32,9 @@ import me.msfjarvis.openpgpktx.util.OpenPgpApi
class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs }
private val settings by lazy(LazyThreadSafetyMode.NONE) {
requireActivity().applicationContext.sharedPrefs
}
private val binding by viewBinding(FragmentKeySelectionBinding::bind)
private val gpgKeySelectAction =
@ -45,13 +47,17 @@ class KeySelectionFragment : Fragment(R.layout.fragment_key_selection) {
gpgIdentifierFile.writeText((keyIds + "").joinToString("\n"))
}
settings.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, true) }
requireActivity().commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name)))
requireActivity()
.commitChange(getString(R.string.git_commit_gpg_id, getString(R.string.app_name)))
}
}
finish()
} else {
requireActivity()
.snackbar(message = getString(R.string.gpg_key_select_mandatory), length = Snackbar.LENGTH_LONG)
.snackbar(
message = getString(R.string.gpg_key_select_mandatory),
length = Snackbar.LENGTH_LONG
)
}
}

View file

@ -35,7 +35,9 @@ import java.io.File
class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
private val settings by lazy(LazyThreadSafetyMode.NONE) { requireActivity().applicationContext.sharedPrefs }
private val settings by lazy(LazyThreadSafetyMode.NONE) {
requireActivity().applicationContext.sharedPrefs
}
private val directorySelectIntent by lazy(LazyThreadSafetyMode.NONE) {
Intent(requireContext(), DirectorySelectionActivity::class.java)
}
@ -65,7 +67,9 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
externalDirectorySelectAction.launch(directorySelectIntent)
}
private val repositoryUsePermGrantedAction = createPermGrantedAction { initializeRepositoryInfo() }
private val repositoryUsePermGrantedAction = createPermGrantedAction {
initializeRepositoryInfo()
}
private val repositoryChangePermGrantedAction = createPermGrantedAction {
repositoryInitAction.launch(directorySelectIntent)
@ -132,7 +136,12 @@ class RepoLocationFragment : Fragment(R.layout.fragment_repo_location) {
dir.isDirectory && // The directory, is really a directory
dir.listFilesRecursively().isNotEmpty() && // The directory contains files
// The directory contains a non-zero number of password files
PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(), sortOrder).isNotEmpty()
PasswordRepository.getPasswords(
dir,
PasswordRepository.getRepositoryDirectory(),
sortOrder
)
.isNotEmpty()
) {
PasswordRepository.closeRepository()
return true

View file

@ -27,6 +27,8 @@ class WelcomeFragment : Fragment(R.layout.fragment_welcome) {
binding.letsGo.setOnClickListener {
parentFragmentManager.performTransactionWithBackStack(CloneFragment.newInstance())
}
binding.settingsButton.setOnClickListener { startActivity(Intent(requireContext(), SettingsActivity::class.java)) }
binding.settingsButton.setOnClickListener {
startActivity(Intent(requireContext(), SettingsActivity::class.java))
}
}
}

View file

@ -77,8 +77,12 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
super.onViewCreated(view, savedInstanceState)
settings = requireContext().sharedPrefs
initializePasswordList()
binding.fab.setOnClickListener { ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET") }
childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) { _, bundle ->
binding.fab.setOnClickListener {
ItemCreationBottomSheet().show(childFragmentManager, "BOTTOM_SHEET")
}
childFragmentManager.setFragmentResultListener(ITEM_CREATION_REQUEST_KEY, viewLifecycleOwner) {
_,
bundle ->
when (bundle.getString(ACTION_KEY)) {
ACTION_FOLDER -> requireStore().createFolder()
ACTION_PASSWORD -> requireStore().createPassword()
@ -88,7 +92,8 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
private fun initializePasswordList() {
val gitDir = File(PasswordRepository.getRepositoryDirectory(), ".git")
val hasGitDir = gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true)
val hasGitDir =
gitDir.exists() && gitDir.isDirectory && (gitDir.listFiles()?.isNotEmpty() == true)
binding.swipeRefresher.setOnRefreshListener {
if (!hasGitDir) {
requireStore().refreshPasswordList()
@ -118,7 +123,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
binding.swipeRefresher.isRefreshing = false
refreshPasswordList()
},
failure = { err -> promptOnErrorHandler(err) { binding.swipeRefresher.isRefreshing = false } },
failure = { err ->
promptOnErrorHandler(err) { binding.swipeRefresher.isRefreshing = false }
},
)
}
}
@ -135,10 +142,16 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
binding.swipeRefresher.isEnabled = selection.isEmpty
if (actionMode == null)
actionMode = requireStore().startSupportActionMode(actionModeCallback) ?: return@onSelectionChanged
actionMode =
requireStore().startSupportActionMode(actionModeCallback) ?: return@onSelectionChanged
if (!selection.isEmpty) {
actionMode!!.title = resources.getQuantityString(R.plurals.delete_title, selection.size(), selection.size())
actionMode!!.title =
resources.getQuantityString(
R.plurals.delete_title,
selection.size(),
selection.size()
)
actionMode!!.invalidate()
} else {
actionMode!!.finish()
@ -171,14 +184,18 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
recyclerView.scrollToPosition(0)
}
scrollTarget != null -> {
scrollTarget?.let { recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it)) }
scrollTarget?.let {
recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it))
}
scrollTarget = null
}
else -> {
// When the result is not filtered and there is a saved scroll position for
// it,
// we try to restore it.
recyclerViewStateToRestore?.let { recyclerView.layoutManager!!.onRestoreInstanceState(it) }
recyclerViewStateToRestore?.let {
recyclerView.layoutManager!!.onRestoreInstanceState(it)
}
recyclerViewStateToRestore = null
}
}
@ -201,7 +218,8 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
// but may be called multiple times if the mode is invalidated.
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val selectedItems = recyclerAdapter.getSelectedItems()
menu.findItem(R.id.menu_edit_password).isVisible = selectedItems.all { it.type == PasswordItem.TYPE_CATEGORY }
menu.findItem(R.id.menu_edit_password).isVisible =
selectedItems.all { it.type == PasswordItem.TYPE_CATEGORY }
menu.findItem(R.id.menu_pin_password).isVisible =
selectedItems.size == 1 && selectedItems[0].type == PasswordItem.TYPE_PASSWORD
return true
@ -227,7 +245,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
}
R.id.menu_pin_password -> {
val passwordItem = recyclerAdapter.getSelectedItems()[0]
shortcutHandler.addPinnedShortcut(passwordItem, passwordItem.createAuthEnabledIntent(requireContext()))
shortcutHandler.addPinnedShortcut(
passwordItem,
passwordItem.createAuthEnabledIntent(requireContext())
)
false
}
else -> false
@ -244,7 +265,8 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
private fun animateFab(show: Boolean) =
with(binding.fab) {
val animation = AnimationUtils.loadAnimation(context, if (show) R.anim.scale_up else R.anim.scale_down)
val animation =
AnimationUtils.loadAnimation(context, if (show) R.anim.scale_up else R.anim.scale_down)
animation.setAnimationListener(
object : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation?) {}
@ -258,7 +280,11 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
}
}
)
animate().rotationBy(if (show) -90f else 90f).setStartDelay(if (show) 100 else 0).setDuration(100).start()
animate()
.rotationBy(if (show) -90f else 90f)
.setStartDelay(if (show) 100 else 0)
.setDuration(100)
.start()
startAnimation(animation)
}
}
@ -269,10 +295,15 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
listener =
object : OnFragmentInteractionListener {
override fun onFragmentInteraction(item: PasswordItem) {
if (settings.getString(PreferenceKeys.SORT_ORDER) == PasswordSortOrder.RECENTLY_USED.name) {
if (settings.getString(PreferenceKeys.SORT_ORDER) ==
PasswordSortOrder.RECENTLY_USED.name
) {
// save the time when password was used
val preferences = context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
preferences.edit { putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString()) }
val preferences =
context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
preferences.edit {
putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString())
}
}
if (item.type == PasswordItem.TYPE_CATEGORY) {
@ -287,7 +318,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
}
}
}
.onFailure { throw ClassCastException("$context must implement OnFragmentInteractionListener") }
.onFailure {
throw ClassCastException("$context must implement OnFragmentInteractionListener")
}
}
private fun requireStore() = requireActivity() as PasswordStore
@ -322,7 +355,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
fun navigateTo(file: File) {
requireStore().clearSearch()
model.navigateTo(file, recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState())
model.navigateTo(
file,
recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()
)
requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
}

View file

@ -82,7 +82,9 @@ class PasswordStore : BaseGitActivity() {
}
private val storagePermissionRequest =
registerForActivityResult(RequestPermission()) { granted -> if (granted) checkLocalRepository() }
registerForActivityResult(RequestPermission()) { granted ->
if (granted) checkLocalRepository()
}
private val directorySelectAction =
registerForActivityResult(StartActivityForResult()) { result ->
@ -128,7 +130,13 @@ class PasswordStore : BaseGitActivity() {
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(this@PasswordStore)
.setTitle(resources.getString(R.string.password_exists_title))
.setMessage(resources.getString(R.string.password_exists_message, destinationLongName, sourceLongName))
.setMessage(
resources.getString(
R.string.password_exists_message,
destinationLongName,
sourceLongName
)
)
.setPositiveButton(R.string.dialog_ok) { _, _ ->
launch(Dispatchers.IO) { moveFile(source, destinationFile) }
}
@ -143,11 +151,16 @@ class PasswordStore : BaseGitActivity() {
1 -> {
val source = File(filesToMove[0])
val basename = source.nameWithoutExtension
val sourceLongName = getLongName(requireNotNull(source.parent), repositoryPath, basename)
val sourceLongName =
getLongName(requireNotNull(source.parent), repositoryPath, basename)
val destinationLongName = getLongName(target.absolutePath, repositoryPath, basename)
withContext(Dispatchers.Main) {
commitChange(
resources.getString(R.string.git_commit_move_text, sourceLongName, destinationLongName),
resources.getString(
R.string.git_commit_move_text,
sourceLongName,
destinationLongName
),
)
}
}
@ -168,8 +181,8 @@ class PasswordStore : BaseGitActivity() {
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
// open search view on search key, or Ctr+F
if ((keyCode == KeyEvent.KEYCODE_SEARCH || keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) &&
!searchItem.isActionViewExpanded
if ((keyCode == KeyEvent.KEYCODE_SEARCH ||
keyCode == KeyEvent.KEYCODE_F && event.isCtrlPressed) && !searchItem.isActionViewExpanded
) {
searchItem.expandActionView()
return true
@ -202,7 +215,9 @@ class PasswordStore : BaseGitActivity() {
model.currentDir.observe(this) { dir ->
val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
supportActionBar!!.apply { if (dir != basePath) title = dir.name else setTitle(R.string.app_name) }
supportActionBar!!.apply {
if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
}
}
}
@ -253,7 +268,8 @@ class PasswordStore : BaseGitActivity() {
val filter = s.trim()
// List the contents of the current directory if the user enters a blank
// search term.
if (filter.isEmpty()) model.navigateTo(newDirectory = model.currentDir.value!!, pushPreviousLocation = false)
if (filter.isEmpty())
model.navigateTo(newDirectory = model.currentDir.value!!, pushPreviousLocation = false)
else model.search(filter)
return true
}
@ -288,7 +304,9 @@ class PasswordStore : BaseGitActivity() {
.setPositiveButton(resources.getString(R.string.dialog_ok), null)
when (id) {
R.id.user_pref -> {
runCatching { startActivity(Intent(this, SettingsActivity::class.java)) }.onFailure { e -> e.printStackTrace() }
runCatching { startActivity(Intent(this, SettingsActivity::class.java)) }.onFailure { e ->
e.printStackTrace()
}
}
R.id.git_push -> {
if (!PasswordRepository.isInitialized) {
@ -372,7 +390,8 @@ class PasswordStore : BaseGitActivity() {
if (localDir != null && settings.getBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)) {
d { "Check, dir: ${localDir.absolutePath}" }
// do not push the fragment if we already have it
if (getPasswordFragment() == null || settings.getBoolean(PreferenceKeys.REPO_CHANGED, false)) {
if (getPasswordFragment() == null || settings.getBoolean(PreferenceKeys.REPO_CHANGED, false)
) {
settings.edit { putBoolean(PreferenceKeys.REPO_CHANGED, false) }
val args = Bundle()
args.putString(REQUEST_ARG_PATH, PasswordRepository.getRepositoryDirectory().absolutePath)
@ -403,7 +422,9 @@ class PasswordStore : BaseGitActivity() {
fun decryptPassword(item: PasswordItem) {
val authDecryptIntent = item.createAuthEnabledIntent(this)
val decryptIntent =
(authDecryptIntent.clone() as Intent).setComponent(ComponentName(this, DecryptActivity::class.java))
(authDecryptIntent.clone() as Intent).setComponent(
ComponentName(this, DecryptActivity::class.java)
)
startActivity(decryptIntent)
@ -439,7 +460,9 @@ class PasswordStore : BaseGitActivity() {
fun deletePasswords(selectedItems: List<PasswordItem>) {
var size = 0
selectedItems.forEach { if (it.file.isFile) size++ else size += it.file.listFilesRecursively().size }
selectedItems.forEach {
if (it.file.isFile) size++ else size += it.file.listFilesRecursively().size
}
if (size == 0) {
selectedItems.map { item -> item.file.deleteRecursively() }
refreshPasswordList()
@ -497,7 +520,10 @@ class PasswordStore : BaseGitActivity() {
* @see [CategoryRenameError]
* @see [isInsideRepository]
*/
private fun renameCategory(oldCategory: PasswordItem, error: CategoryRenameError = CategoryRenameError.None) {
private fun renameCategory(
oldCategory: PasswordItem,
error: CategoryRenameError = CategoryRenameError.None
) {
val view = layoutInflater.inflate(R.layout.folder_dialog_fragment, null)
val newCategoryEditText = view.findViewById<TextInputEditText>(R.id.folder_name_text)
@ -513,16 +539,19 @@ class PasswordStore : BaseGitActivity() {
.setPositiveButton(R.string.dialog_ok) { _, _ ->
val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}")
when {
newCategoryEditText.text.isNullOrBlank() -> renameCategory(oldCategory, CategoryRenameError.EmptyField)
newCategoryEditText.text.isNullOrBlank() ->
renameCategory(oldCategory, CategoryRenameError.EmptyField)
newCategory.exists() -> renameCategory(oldCategory, CategoryRenameError.CategoryExists)
!newCategory.isInsideRepository() -> renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo)
!newCategory.isInsideRepository() ->
renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo)
else ->
lifecycleScope.launch(Dispatchers.IO) {
moveFile(oldCategory.file, newCategory)
// associate the new category with the last category's timestamp in
// history
val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
val preference =
getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
val timestamp = preference.getString(oldCategory.file.absolutePath.base64())
if (timestamp != null) {
preference.edit {
@ -533,7 +562,11 @@ class PasswordStore : BaseGitActivity() {
withContext(Dispatchers.Main) {
commitChange(
resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name),
resources.getString(
R.string.git_commit_move_text,
oldCategory.name,
newCategory.name
),
)
}
}
@ -584,7 +617,9 @@ class PasswordStore : BaseGitActivity() {
// Recursively list all files (not directories) below `source`, then
// obtain the corresponding target file by resolving the relative path
// starting at the destination folder.
source.listFilesRecursively().associateWith { destinationFile.resolve(it.relativeTo(source)) }
source.listFilesRecursively().associateWith {
destinationFile.resolve(it.relativeTo(source))
}
} else {
mapOf(source to destinationFile)
}

View file

@ -28,7 +28,9 @@ private val WEB_ADDRESS_REGEX = Patterns.WEB_URL.toRegex()
class ProxySelectorActivity : AppCompatActivity() {
private val binding by viewBinding(ActivityProxySelectorBinding::inflate)
private val proxyPrefs by lazy(LazyThreadSafetyMode.NONE) { applicationContext.getEncryptedProxyPrefs() }
private val proxyPrefs by lazy(LazyThreadSafetyMode.NONE) {
applicationContext.getEncryptedProxyPrefs()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -36,7 +38,9 @@ class ProxySelectorActivity : AppCompatActivity() {
with(binding) {
proxyHost.setText(proxyPrefs.getString(PreferenceKeys.PROXY_HOST))
proxyUser.setText(proxyPrefs.getString(PreferenceKeys.PROXY_USERNAME))
proxyPrefs.getInt(PreferenceKeys.PROXY_PORT, -1).takeIf { it != -1 }?.let { proxyPort.setText("$it") }
proxyPrefs.getInt(PreferenceKeys.PROXY_PORT, -1).takeIf { it != -1 }?.let {
proxyPort.setText("$it")
}
proxyPassword.setText(proxyPrefs.getString(PreferenceKeys.PROXY_PASSWORD))
save.setOnClickListener { saveSettings() }
proxyHost.doOnTextChanged { text, _, _, _ ->
@ -54,10 +58,18 @@ class ProxySelectorActivity : AppCompatActivity() {
private fun saveSettings() {
proxyPrefs.edit {
binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyHost = it }
binding.proxyUser.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyUsername = it }
binding.proxyPort.text?.toString()?.takeIf { it.isNotEmpty() }?.let { GitSettings.proxyPort = it.toInt() }
binding.proxyPassword.text?.toString()?.takeIf { it.isNotEmpty() }.let { GitSettings.proxyPassword = it }
binding.proxyHost.text?.toString()?.takeIf { it.isNotEmpty() }.let {
GitSettings.proxyHost = it
}
binding.proxyUser.text?.toString()?.takeIf { it.isNotEmpty() }.let {
GitSettings.proxyUsername = it
}
binding.proxyPort.text?.toString()?.takeIf { it.isNotEmpty() }?.let {
GitSettings.proxyPort = it.toInt()
}
binding.proxyPassword.text?.toString()?.takeIf { it.isNotEmpty() }.let {
GitSettings.proxyPassword = it
}
}
ProxyUtils.setDefaultProxy()
Handler(Looper.getMainLooper()).postDelayed(500) { finish() }

View file

@ -59,13 +59,18 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
val appLabel = it.first
val supportDescription =
when (it.second) {
BrowserAutofillSupportLevel.None -> activity.getString(R.string.oreo_autofill_no_support)
BrowserAutofillSupportLevel.FlakyFill -> activity.getString(R.string.oreo_autofill_flaky_fill_support)
BrowserAutofillSupportLevel.None ->
activity.getString(R.string.oreo_autofill_no_support)
BrowserAutofillSupportLevel.FlakyFill ->
activity.getString(R.string.oreo_autofill_flaky_fill_support)
BrowserAutofillSupportLevel.PasswordFill ->
activity.getString(R.string.oreo_autofill_password_fill_support)
BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility ->
activity.getString(R.string.oreo_autofill_password_fill_and_conditional_save_support)
BrowserAutofillSupportLevel.GeneralFill -> activity.getString(R.string.oreo_autofill_general_fill_support)
activity.getString(
R.string.oreo_autofill_password_fill_and_conditional_save_support
)
BrowserAutofillSupportLevel.GeneralFill ->
activity.getString(R.string.oreo_autofill_general_fill_support)
BrowserAutofillSupportLevel.GeneralFillAndSave ->
activity.getString(R.string.oreo_autofill_general_fill_and_save_support)
}
@ -102,8 +107,10 @@ class AutofillSettings(private val activity: FragmentActivity) : SettingsProvide
false
}
}
val values = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_values)
val titles = activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_entries)
val values =
activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_values)
val titles =
activity.resources.getStringArray(R.array.oreo_autofill_directory_structure_entries)
val items = values.zip(titles).map { SelectionItem(it.first, it.second, null) }
singleChoice(PreferenceKeys.OREO_AUTOFILL_DIRECTORY_STRUCTURE, items) {
initialSelection = DirectoryStructure.DEFAULT.value

View file

@ -39,7 +39,9 @@ class DirectorySelectionActivity : AppCompatActivity() {
MaterialAlertDialogBuilder(this)
.setTitle(resources.getString(R.string.sdcard_root_warning_title))
.setMessage(resources.getString(R.string.sdcard_root_warning_message))
.setPositiveButton(resources.getString(R.string.sdcard_root_warning_remove_everything)) { _, _ ->
.setPositiveButton(resources.getString(R.string.sdcard_root_warning_remove_everything)) {
_,
_ ->
prefs.edit { putString(PreferenceKeys.GIT_EXTERNAL_REPO, uri.path) }
}
.setNegativeButton(R.string.dialog_cancel, null)

View file

@ -26,7 +26,8 @@ class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider
builder.apply {
val themeValues = activity.resources.getStringArray(R.array.app_theme_values)
val themeOptions = activity.resources.getStringArray(R.array.app_theme_options)
val themeItems = themeValues.zip(themeOptions).map { SelectionItem(it.first, it.second, null) }
val themeItems =
themeValues.zip(themeOptions).map { SelectionItem(it.first, it.second, null) }
singleChoice(PreferenceKeys.APP_THEME, themeItems) {
initialSelection = activity.resources.getString(R.string.app_theme_def)
titleRes = R.string.pref_app_theme_title
@ -64,7 +65,8 @@ class GeneralSettings(private val activity: FragmentActivity) : SettingsProvider
defaultValue = false
enabled = canAuthenticate
summaryRes =
if (canAuthenticate) R.string.pref_biometric_auth_summary else R.string.pref_biometric_auth_summary_error
if (canAuthenticate) R.string.pref_biometric_auth_summary
else R.string.pref_biometric_auth_summary_error
onClick {
enabled = false
val isChecked = checked

View file

@ -37,7 +37,9 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys
class RepositorySettings(private val activity: FragmentActivity) : SettingsProvider {
private val encryptedPreferences by lazy(LazyThreadSafetyMode.NONE) { activity.getEncryptedGitPrefs() }
private val encryptedPreferences by lazy(LazyThreadSafetyMode.NONE) {
activity.getEncryptedGitPrefs()
}
private fun <T : FragmentActivity> launchActivity(clazz: Class<T>) {
activity.startActivity(Intent(activity, clazz))
@ -47,7 +49,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
MaterialAlertDialogBuilder(activity)
.setTitle(activity.resources.getString(R.string.external_repository_dialog_title))
.setMessage(activity.resources.getString(R.string.external_repository_dialog_text))
.setPositiveButton(R.string.dialog_ok) { _, _ -> launchActivity(DirectorySelectionActivity::class.java) }
.setPositiveButton(R.string.dialog_ok) { _, _ ->
launchActivity(DirectorySelectionActivity::class.java)
}
.setNegativeButton(R.string.dialog_cancel, null)
.show()
}
@ -130,7 +134,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
}
pref(PreferenceKeys.SSH_OPENKEYSTORE_CLEAR_KEY_ID) {
titleRes = R.string.pref_title_openkeystore_clear_keyid
visible = activity.sharedPrefs.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty() ?: false
visible =
activity.sharedPrefs.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID)?.isNotEmpty()
?: false
onClick {
activity.sharedPrefs.edit { putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null) }
visible = false
@ -160,7 +166,9 @@ class RepositorySettings(private val activity: FragmentActivity) : SettingsProvi
removeDynamicShortcuts(dynamicShortcuts.map { it.id }.toMutableList())
}
}
activity.sharedPrefs.edit { putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false) }
activity.sharedPrefs.edit {
putBoolean(PreferenceKeys.REPOSITORY_INITIALIZED, false)
}
dialogInterface.cancel()
activity.finish()
}

View file

@ -68,7 +68,9 @@ class SettingsActivity : AppCompatActivity() {
getString(subScreen.titleRes)
}
}
savedInstanceState?.getParcelable<PreferencesAdapter.SavedState>("adapter")?.let(adapter::loadSavedState)
savedInstanceState
?.getParcelable<PreferencesAdapter.SavedState>("adapter")
?.let(adapter::loadSavedState)
binding.preferenceRecyclerView.adapter = adapter
}

View file

@ -20,7 +20,9 @@ class ShowSshKeyFragment : DialogFragment() {
return MaterialAlertDialogBuilder(requireActivity()).run {
setMessage(getString(R.string.ssh_keygen_message, publicKey))
setTitle(R.string.your_public_key)
setNegativeButton(R.string.ssh_keygen_later) { _, _ -> (activity as? SshKeyGenActivity)?.finish() }
setNegativeButton(R.string.ssh_keygen_later) { _, _ ->
(activity as? SshKeyGenActivity)?.finish()
}
setPositiveButton(R.string.ssh_keygen_share) { _, _ ->
val sendIntent =
Intent().apply {

View file

@ -30,9 +30,15 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
private enum class KeyGenType(val generateKey: suspend (requireAuthentication: Boolean) -> Unit) {
Rsa({ requireAuthentication -> SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication) }),
Ecdsa({ requireAuthentication -> SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication) }),
Ed25519({ requireAuthentication -> SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication) }),
Rsa({ requireAuthentication ->
SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Rsa, requireAuthentication)
}),
Ecdsa({ requireAuthentication ->
SshKey.generateKeystoreNativeKey(SshKey.Algorithm.Ecdsa, requireAuthentication)
}),
Ed25519({ requireAuthentication ->
SshKey.generateKeystoreWrappedEd25519Key(requireAuthentication)
}),
}
class SshKeyGenActivity : AppCompatActivity() {
@ -50,7 +56,9 @@ class SshKeyGenActivity : AppCompatActivity() {
MaterialAlertDialogBuilder(this@SshKeyGenActivity).run {
setTitle(R.string.ssh_keygen_existing_title)
setMessage(R.string.ssh_keygen_existing_message)
setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ -> lifecycleScope.launch { generate() } }
setPositiveButton(R.string.ssh_keygen_existing_replace) { _, _ ->
lifecycleScope.launch { generate() }
}
setNegativeButton(R.string.ssh_keygen_existing_keep) { _, _ -> finish() }
show()
}

View file

@ -26,7 +26,12 @@ class SshKeyImportActivity : AppCompatActivity() {
}
runCatching {
SshKey.import(uri)
Toast.makeText(this, resources.getString(R.string.ssh_key_success_dialog_title), Toast.LENGTH_LONG).show()
Toast.makeText(
this,
resources.getString(R.string.ssh_key_success_dialog_title),
Toast.LENGTH_LONG
)
.show()
setResult(RESULT_OK)
finish()
}

View file

@ -19,7 +19,8 @@ import dev.msfjarvis.aps.R
object BiometricAuthenticator {
private const val TAG = "BiometricAuthenticator"
private const val validAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
private const val validAuthenticators =
Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
sealed class Result {
data class Success(val cryptoObject: BiometricPrompt.CryptoObject?) : Result()
@ -29,7 +30,8 @@ object BiometricAuthenticator {
}
fun canAuthenticate(activity: FragmentActivity): Boolean {
return BiometricManager.from(activity).canAuthenticate(validAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS
return BiometricManager.from(activity).canAuthenticate(validAuthenticators) ==
BiometricManager.BIOMETRIC_SUCCESS
}
fun authenticate(
@ -55,7 +57,11 @@ object BiometricAuthenticator {
BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL -> {
Result.HardwareUnavailableOrDisabled
}
else -> Result.Failure(errorCode, activity.getString(R.string.biometric_auth_error_reason, errString))
else ->
Result.Failure(
errorCode,
activity.getString(R.string.biometric_auth_error_reason, errString)
)
}
)
}
@ -77,7 +83,11 @@ object BiometricAuthenticator {
.setTitle(activity.getString(dialogTitleRes))
.setAllowedAuthenticators(validAuthenticators)
.build()
BiometricPrompt(activity, ContextCompat.getMainExecutor(activity.applicationContext), authCallback)
BiometricPrompt(
activity,
ContextCompat.getMainExecutor(activity.applicationContext),
authCallback
)
.authenticate(promptInfo)
} else {
callback(Result.HardwareUnavailableOrDisabled)

View file

@ -61,7 +61,11 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
}
}
private fun makeMatchDataset(context: Context, file: File, imeSpec: InlinePresentationSpec?): Dataset? {
private fun makeMatchDataset(
context: Context,
file: File,
imeSpec: InlinePresentationSpec?
): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null
val metadata = makeFillMatchMetadata(context, file)
val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
@ -82,12 +86,21 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata, imeSpec)
}
private fun makeFillOtpFromSmsDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? {
private fun makeFillOtpFromSmsDataset(
context: Context,
imeSpec: InlinePresentationSpec?
): Dataset? {
if (!scenario.hasFieldsToFillOn(AutofillAction.FillOtpFromSms)) return null
if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
val metadata = makeFillOtpFromSmsMetadata(context)
val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
return makeIntentDataset(context, AutofillAction.FillOtpFromSms, intentSender, metadata, imeSpec)
return makeIntentDataset(
context,
AutofillAction.FillOtpFromSms,
intentSender,
metadata,
imeSpec
)
}
private fun makePublisherChangedDataset(
@ -150,7 +163,12 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
addDataset(it)
}
if (datasetCount == 0) return null
setHeader(makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))))
setHeader(
makeRemoteView(
context,
makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))
)
)
makeSaveInfo()?.let { setSaveInfo(it) }
setClientState(clientState)
setIgnoredIds(*ignoredIds.toTypedArray())
@ -177,7 +195,11 @@ class Api30AutofillResponseBuilder(form: FillableForm) {
}
/** Creates and returns a suitable [FillResponse] to the Autofill framework. */
fun fillCredentials(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) {
fun fillCredentials(
context: Context,
inlineSuggestionsRequest: InlineSuggestionsRequest?,
callback: FillCallback
) {
AutofillMatcher.getMatchesFor(context, formOrigin)
.fold(
success = { matchedFiles ->

View file

@ -35,7 +35,9 @@ private fun Context.matchPreferences(formOrigin: FormOrigin): SharedPreferences
}
class AutofillPublisherChangedException(val formOrigin: FormOrigin) :
Exception("The publisher of '${formOrigin.identifier}' changed since an entry was first matched with this app") {
Exception(
"The publisher of '${formOrigin.identifier}' changed since an entry was first matched with this app"
) {
init {
require(formOrigin is FormOrigin.App)
@ -50,10 +52,12 @@ class AutofillMatcher {
private const val MAX_NUM_MATCHES = 10
private const val PREFERENCE_PREFIX_TOKEN = "token;"
private fun tokenKey(formOrigin: FormOrigin.App) = "$PREFERENCE_PREFIX_TOKEN${formOrigin.identifier}"
private fun tokenKey(formOrigin: FormOrigin.App) =
"$PREFERENCE_PREFIX_TOKEN${formOrigin.identifier}"
private const val PREFERENCE_PREFIX_MATCHES = "matches;"
private fun matchesKey(formOrigin: FormOrigin) = "$PREFERENCE_PREFIX_MATCHES${formOrigin.identifier}"
private fun matchesKey(formOrigin: FormOrigin) =
"$PREFERENCE_PREFIX_MATCHES${formOrigin.identifier}"
private fun hasFormOriginHashChanged(context: Context, formOrigin: FormOrigin): Boolean {
return when (formOrigin) {
@ -61,7 +65,8 @@ class AutofillMatcher {
is FormOrigin.App -> {
val packageName = formOrigin.identifier
val certificatesHash = computeCertificatesHash(context, packageName)
val storedCertificatesHash = context.autofillAppMatches.getString(tokenKey(formOrigin), null) ?: return false
val storedCertificatesHash =
context.autofillAppMatches.getString(tokenKey(formOrigin), null) ?: return false
val hashHasChanged = certificatesHash != storedCertificatesHash
if (hashHasChanged) {
e { "$packageName: stored=$storedCertificatesHash, new=$certificatesHash" }
@ -91,15 +96,21 @@ class AutofillMatcher {
* time the user associated an entry with it, an [AutofillPublisherChangedException] will be
* thrown.
*/
fun getMatchesFor(context: Context, formOrigin: FormOrigin): Result<List<File>, AutofillPublisherChangedException> {
fun getMatchesFor(
context: Context,
formOrigin: FormOrigin
): Result<List<File>, AutofillPublisherChangedException> {
if (hasFormOriginHashChanged(context, formOrigin)) {
return Err(AutofillPublisherChangedException(formOrigin))
}
val matchPreferences = context.matchPreferences(formOrigin)
val matchedFiles = matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
val matchedFiles =
matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
return Ok(
matchedFiles.filter { it.exists() }.also { validFiles ->
matchPreferences.edit { putStringSet(matchesKey(formOrigin), validFiles.map { it.absolutePath }.toSet()) }
matchPreferences.edit {
putStringSet(matchesKey(formOrigin), validFiles.map { it.absolutePath }.toSet())
}
}
)
}
@ -127,7 +138,8 @@ class AutofillMatcher {
throw AutofillPublisherChangedException(formOrigin)
}
val matchPreferences = context.matchPreferences(formOrigin)
val matchedFiles = matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
val matchedFiles =
matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) }
val newFiles = setOf(file.absoluteFile).union(matchedFiles)
if (newFiles.size > MAX_NUM_MATCHES) {
Toast.makeText(
@ -138,7 +150,9 @@ class AutofillMatcher {
.show()
return
}
matchPreferences.edit { putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet()) }
matchPreferences.edit {
putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet())
}
storeFormOriginHash(context, formOrigin)
d { "Stored match for $formOrigin" }
}
@ -153,7 +167,8 @@ class AutofillMatcher {
delete: Collection<File> = emptyList()
) {
val deletePathList = delete.map { it.absolutePath }
val oldNewPathMap = moveFromTo.mapValues { it.value.absolutePath }.mapKeys { it.key.absolutePath }
val oldNewPathMap =
moveFromTo.mapValues { it.value.absolutePath }.mapKeys { it.key.absolutePath }
for (prefs in listOf(context.autofillAppMatches, context.autofillWebMatches)) {
for ((key, value) in prefs.all) {
if (!key.startsWith(PREFERENCE_PREFIX_MATCHES)) continue

View file

@ -96,7 +96,8 @@ enum class DirectoryStructure(val value: String) {
when (this) {
EncryptedUsername -> null
FileBased -> file.nameWithoutExtension.takeIf { file.parentFile != null }
DirectoryBased -> file.parentFile?.let { parentFile -> "${parentFile.name}/${file.nameWithoutExtension}" }
DirectoryBased ->
file.parentFile?.let { parentFile -> "${parentFile.name}/${file.nameWithoutExtension}" }
?: file.nameWithoutExtension
}
@ -138,7 +139,8 @@ object AutofillPreferences {
directoryStructure: DirectoryStructure
): Credentials {
// Always give priority to a username stored in the encrypted extras
val username = entry.username ?: directoryStructure.getUsernameFor(file) ?: context.getDefaultUsername()
val username =
entry.username ?: directoryStructure.getUsernameFor(file) ?: context.getDefaultUsername()
return Credentials(username, entry.password, entry.totp.value)
}
}

View file

@ -154,7 +154,10 @@ class AutofillResponseBuilder(form: FillableForm) {
if (datasetCount == 0) return null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
setHeader(
makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true)))
makeRemoteView(
context,
makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true))
)
)
}
makeSaveInfo()?.let { setSaveInfo(it) }

View file

@ -51,13 +51,15 @@ fun makeInlinePresentation(
if (UiVersions.INLINE_UI_VERSION_1 !in UiVersions.getVersions(imeSpec.style)) return null
val launchIntent = PendingIntent.getActivity(context, 0, Intent(context, PasswordStore::class.java), 0)
val launchIntent =
PendingIntent.getActivity(context, 0, Intent(context, PasswordStore::class.java), 0)
val slice =
InlineSuggestionUi.newContentBuilder(launchIntent).run {
setTitle(metadata.title)
if (metadata.subtitle != null) setSubtitle(metadata.subtitle)
setContentDescription(
if (metadata.subtitle != null) "${metadata.title} - ${metadata.subtitle}" else metadata.title
if (metadata.subtitle != null) "${metadata.title} - ${metadata.subtitle}"
else metadata.title
)
setStartIcon(Icon.createWithResource(context, metadata.iconRes))
build().slice
@ -69,13 +71,19 @@ fun makeInlinePresentation(
fun makeFillMatchMetadata(context: Context, file: File): DatasetMetadata {
val directoryStructure = AutofillPreferences.directoryStructure(context)
val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory())
val title = directoryStructure.getIdentifierFor(relativeFile) ?: directoryStructure.getAccountPartFor(relativeFile)!!
val title =
directoryStructure.getIdentifierFor(relativeFile)
?: directoryStructure.getAccountPartFor(relativeFile)!!
val subtitle = directoryStructure.getAccountPartFor(relativeFile)
return DatasetMetadata(title, subtitle, R.drawable.ic_person_black_24dp)
}
fun makeSearchAndFillMetadata(context: Context) =
DatasetMetadata(context.getString(R.string.oreo_autofill_search_in_store), null, R.drawable.ic_search_black_24dp)
DatasetMetadata(
context.getString(R.string.oreo_autofill_search_in_store),
null,
R.drawable.ic_search_black_24dp
)
fun makeGenerateAndFillMetadata(context: Context) =
DatasetMetadata(
@ -85,7 +93,11 @@ fun makeGenerateAndFillMetadata(context: Context) =
)
fun makeFillOtpFromSmsMetadata(context: Context) =
DatasetMetadata(context.getString(R.string.oreo_autofill_fill_otp_from_sms), null, R.drawable.ic_autofill_sms)
DatasetMetadata(
context.getString(R.string.oreo_autofill_fill_otp_from_sms),
null,
R.drawable.ic_autofill_sms
)
fun makeEmptyMetadata() = DatasetMetadata("PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher)

View file

@ -17,7 +17,8 @@ sealed class GpgIdentifier {
if (identifier.isEmpty()) return null
// Match long key IDs:
// FF22334455667788 or 0xFF22334455667788
val maybeLongKeyId = identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{16}".toRegex()) }
val maybeLongKeyId =
identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{16}".toRegex()) }
if (maybeLongKeyId != null) {
val keyId = maybeLongKeyId.toULong(16)
return KeyId(keyId.toLong())
@ -25,7 +26,8 @@ sealed class GpgIdentifier {
// Match fingerprints:
// FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899
val maybeFingerprint = identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{40}".toRegex()) }
val maybeFingerprint =
identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F0-9]{40}".toRegex()) }
if (maybeFingerprint != null) {
// Truncating to the long key ID is not a security issue since OpenKeychain only
// accepts

View file

@ -41,7 +41,11 @@ fun <T : View> AlertDialog.requestInputFocusOnView(@IdRes id: Int) {
setOnShowListener {
findViewById<T>(id)?.apply {
setOnFocusChangeListener { v, _ ->
v.post { context.getSystemService<InputMethodManager>()?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT) }
v.post {
context
.getSystemService<InputMethodManager>()
?.showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
}
}
requestFocus()
}
@ -64,7 +68,8 @@ fun Context.getEncryptedProxyPrefs() = getEncryptedPrefs("http_proxy")
/** Get an instance of [EncryptedSharedPreferences] with the given [fileName] */
private fun Context.getEncryptedPrefs(fileName: String): SharedPreferences {
val masterKeyAlias = MasterKey.Builder(applicationContext).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val masterKeyAlias =
MasterKey.Builder(applicationContext).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
return EncryptedSharedPreferences.create(
applicationContext,
fileName,

View file

@ -20,8 +20,10 @@ import kotlin.reflect.KProperty
* Imported from
* https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c
*/
class FragmentViewBindingDelegate<T : ViewBinding>(val fragment: Fragment, val viewBindingFactory: (View) -> T) :
ReadOnlyProperty<Fragment, T> {
class FragmentViewBindingDelegate<T : ViewBinding>(
val fragment: Fragment,
val viewBindingFactory: (View) -> T
) : ReadOnlyProperty<Fragment, T> {
private var binding: T? = null
@ -51,7 +53,9 @@ class FragmentViewBindingDelegate<T : ViewBinding>(val fragment: Fragment, val v
val lifecycle = fragment.viewLifecycleOwner.lifecycle
if (!lifecycle.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) {
throw IllegalStateException("Should not attempt to get bindings when Fragment views are destroyed.")
throw IllegalStateException(
"Should not attempt to get bindings when Fragment views are destroyed."
)
}
return viewBindingFactory(thisRef.requireView()).also { this.binding = it }
@ -61,5 +65,6 @@ class FragmentViewBindingDelegate<T : ViewBinding>(val fragment: Fragment, val v
fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T) =
FragmentViewBindingDelegate(this, viewBindingFactory)
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(crossinline bindingInflater: (LayoutInflater) -> T) =
lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) }
inline fun <T : ViewBinding> AppCompatActivity.viewBinding(
crossinline bindingInflater: (LayoutInflater) -> T
) = lazy(LazyThreadSafetyMode.NONE) { bindingInflater.invoke(layoutInflater) }

View file

@ -14,7 +14,8 @@ import java.net.UnknownHostException
/**
* Supertype for all Git-related [Exception] s that can be thrown by [GitCommandExecutor.execute].
*/
sealed class GitException(@StringRes res: Int, vararg fmt: String) : Exception(buildMessage(res, *fmt)) {
sealed class GitException(@StringRes res: Int, vararg fmt: String) :
Exception(buildMessage(res, *fmt)) {
override val message = super.message!!

View file

@ -74,11 +74,13 @@ class GitCommandExecutor(
// Code imported (modified) from Gerrit PushOp, license Apache v2
for (rru in result.remoteUpdates) {
when (rru.status) {
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD -> throw PushException.NonFastForward
RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD ->
throw PushException.NonFastForward
RemoteRefUpdate.Status.REJECTED_NODELETE,
RemoteRefUpdate.Status.REJECTED_REMOTE_CHANGED,
RemoteRefUpdate.Status.NON_EXISTING,
RemoteRefUpdate.Status.NOT_ATTEMPTED, -> throw PushException.Generic(rru.status.name)
RemoteRefUpdate.Status.NOT_ATTEMPTED, ->
throw PushException.Generic(rru.status.name)
RemoteRefUpdate.Status.REJECTED_OTHER_REASON -> {
throw if ("non-fast-forward" == rru.message) {
PushException.RemoteRejected

View file

@ -15,4 +15,9 @@ import java.util.Date
* @property authorName name of the commit's author without email address.
* @property time time when the commit was created.
*/
data class GitCommit(val hash: String, val shortMessage: String, val authorName: String, val time: Date)
data class GitCommit(
val hash: String,
val shortMessage: String,
val authorName: String,
val time: Date
)

View file

@ -40,7 +40,9 @@ class GitLogModel {
// Additionally, tests with 1000 commits in the log have not produced a significant delay in the
// user experience.
private val cache: MutableList<GitCommit> by lazy(LazyThreadSafetyMode.NONE) {
commits().map { GitCommit(it.hash, it.shortMessage, it.authorIdent.name, it.time) }.toMutableList()
commits()
.map { GitCommit(it.hash, it.shortMessage, it.authorIdent.name, it.time) }
.toMutableList()
}
val size = cache.size

View file

@ -11,13 +11,17 @@ import org.eclipse.jgit.api.RebaseCommand
import org.eclipse.jgit.api.ResetCommand
import org.eclipse.jgit.lib.RepositoryState
class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) : GitOperation(callingActivity) {
class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
private val merging = repository.repositoryState == RepositoryState.MERGING
private val resetCommands =
arrayOf(
// git checkout -b conflict-branch
git.checkout().setCreateBranch(true).setName("conflicting-$remoteBranch-${System.currentTimeMillis()}"),
git
.checkout()
.setCreateBranch(true)
.setName("conflicting-$remoteBranch-${System.currentTimeMillis()}"),
// push the changes
git.push().setRemote("origin"),
// switch back to ${gitBranch}
@ -47,8 +51,12 @@ class BreakOutOfDetached(callingActivity: ContinuationContainerActivity) : GitOp
if (!git.repository.repositoryState.isRebasing && !merging) {
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.git_abort_and_push_title))
.setMessage(callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded))
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ -> callingActivity.finish() }
.setMessage(
callingActivity.resources.getString(R.string.git_break_out_of_detached_unneeded)
)
.setPositiveButton(callingActivity.resources.getString(R.string.dialog_ok)) { _, _ ->
callingActivity.finish()
}
.show()
false
} else {

View file

@ -14,7 +14,8 @@ import org.eclipse.jgit.api.GitCommand
* @param uri URL to clone the repository from
* @param callingActivity the calling activity
*/
class CloneOperation(callingActivity: ContinuationContainerActivity, uri: String) : GitOperation(callingActivity) {
class CloneOperation(callingActivity: ContinuationContainerActivity, uri: String) :
GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> =
arrayOf(

View file

@ -24,7 +24,8 @@ import dev.msfjarvis.aps.util.settings.PreferenceKeys
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CredentialFinder(val callingActivity: FragmentActivity, val authMode: AuthMode) : InteractivePasswordFinder() {
class CredentialFinder(val callingActivity: FragmentActivity, val authMode: AuthMode) :
InteractivePasswordFinder() {
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
val gitOperationPrefs = callingActivity.getEncryptedGitPrefs()
@ -49,18 +50,22 @@ class CredentialFinder(val callingActivity: FragmentActivity, val authMode: Auth
rememberRes = R.string.git_operation_remember_password
errorRes = R.string.git_operation_wrong_password
}
else -> throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
else ->
throw IllegalStateException("Only SshKey and Password connection mode ask for passwords")
}
if (isRetry) gitOperationPrefs.edit { remove(credentialPref) }
val storedCredential = gitOperationPrefs.getString(credentialPref, null)
if (storedCredential == null) {
val layoutInflater = LayoutInflater.from(callingActivity)
@SuppressLint("InflateParams") val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
val credentialLayout = dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
@SuppressLint("InflateParams")
val dialogView = layoutInflater.inflate(R.layout.git_credential_layout, null)
val credentialLayout =
dialogView.findViewById<TextInputLayout>(R.id.git_auth_passphrase_layout)
val editCredential = dialogView.findViewById<TextInputEditText>(R.id.git_auth_credential)
editCredential.setHint(hintRes)
val rememberCredential = dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
val rememberCredential =
dialogView.findViewById<MaterialCheckBox>(R.id.git_auth_remember_credential)
rememberCredential.setText(rememberRes)
if (isRetry) {
credentialLayout.error = callingActivity.resources.getString(errorRes)

View file

@ -60,7 +60,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
private val authActivity
get() = callingActivity as ContinuationContainerActivity
private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) : CredentialsProvider() {
private class HttpsCredentialsProvider(private val passwordFinder: PasswordFinder) :
CredentialsProvider() {
private var cachedPassword: CharArray? = null
@ -72,7 +73,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
is CredentialItem.Username -> item.value = uri?.user
is CredentialItem.Password -> {
item.value =
cachedPassword?.clone() ?: passwordFinder.reqPassword(null).also { cachedPassword = it.clone() }
cachedPassword?.clone()
?: passwordFinder.reqPassword(null).also { cachedPassword = it.clone() }
}
else -> UnsupportedCredentialItem(uri, item.javaClass.name)
}
@ -102,7 +104,10 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
.onFailure { e -> e(e) }
}
private fun registerAuthProviders(authMethod: SshAuthMethod, credentialsProvider: CredentialsProvider? = null) {
private fun registerAuthProviders(
authMethod: SshAuthMethod,
credentialsProvider: CredentialsProvider? = null
) {
sshSessionFactory = SshjSessionFactory(authMethod, hostKeyFile)
commands.filterIsInstance<TransportCommand<*, *>>().forEach { command ->
command.setTransportConfigCallback { transport: Transport ->
@ -132,12 +137,12 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
MaterialAlertDialogBuilder(callingActivity)
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
.setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
.setPositiveButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)) { _, _ ->
getSshKey(false)
}
.setNegativeButton(callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)) { _, _ ->
getSshKey(true)
}
.setPositiveButton(
callingActivity.resources.getString(R.string.ssh_preferences_dialog_import)
) { _, _ -> getSshKey(false) }
.setNegativeButton(
callingActivity.resources.getString(R.string.ssh_preferences_dialog_generate)
) { _, _ -> getSshKey(true) }
.setNeutralButton(callingActivity.resources.getString(R.string.dialog_cancel)) { _, _ ->
// Finish the blank GitActivity so user doesn't have to press back
callingActivity.finish()
@ -153,9 +158,10 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
val result =
withContext(Dispatchers.Main) {
suspendCoroutine<BiometricAuthenticator.Result> { cont ->
BiometricAuthenticator.authenticate(callingActivity, R.string.biometric_prompt_title_ssh_auth) {
if (it !is BiometricAuthenticator.Result.Failure) cont.resume(it)
}
BiometricAuthenticator.authenticate(
callingActivity,
R.string.biometric_prompt_title_ssh_auth
) { if (it !is BiometricAuthenticator.Result.Failure) cont.resume(it) }
}
}
when (result) {
@ -193,7 +199,8 @@ abstract class GitOperation(protected val callingActivity: FragmentActivity) {
}
AuthMode.OpenKeychain -> registerAuthProviders(SshAuthMethod.OpenKeychain(authActivity))
AuthMode.Password -> {
val httpsCredentialProvider = HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))
val httpsCredentialProvider =
HttpsCredentialsProvider(CredentialFinder(callingActivity, AuthMode.Password))
registerAuthProviders(SshAuthMethod.Password(authActivity), httpsCredentialProvider)
}
AuthMode.None -> {}

View file

@ -7,7 +7,8 @@ package dev.msfjarvis.aps.util.git.operation
import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity
import org.eclipse.jgit.api.GitCommand
class PushOperation(callingActivity: ContinuationContainerActivity) : GitOperation(callingActivity) {
class PushOperation(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
override val commands: Array<GitCommand<out Any>> =
arrayOf(

View file

@ -7,7 +7,8 @@ package dev.msfjarvis.aps.util.git.operation
import dev.msfjarvis.aps.util.git.sshj.ContinuationContainerActivity
import org.eclipse.jgit.api.ResetCommand
class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) : GitOperation(callingActivity) {
class ResetToRemoteOperation(callingActivity: ContinuationContainerActivity) :
GitOperation(callingActivity) {
override val commands =
arrayOf(

View file

@ -106,7 +106,8 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
val response = sshPublicKeyResponse.response as SshPublicKeyResponse
val sshPublicKey = response.sshPublicKey!!
publicKey =
parseSshPublicKey(sshPublicKey) ?: throw IllegalStateException("OpenKeychain API returned invalid SSH key")
parseSshPublicKey(sshPublicKey)
?: throw IllegalStateException("OpenKeychain API returned invalid SSH key")
}
is ApiResponse.NoSuchKey ->
if (isRetry) {
@ -122,13 +123,17 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
private suspend fun selectKey() {
when (val keySelectionResponse = executeApiRequest(KeySelectionRequest())) {
is ApiResponse.Success -> keyId = (keySelectionResponse.response as KeySelectionResponse).keyId
is ApiResponse.Success ->
keyId = (keySelectionResponse.response as KeySelectionResponse).keyId
is ApiResponse.GeneralError -> throw keySelectionResponse.exception
is ApiResponse.NoSuchKey -> throw keySelectionResponse.exception
}
}
private suspend fun executeApiRequest(request: Request, resultOfUserInteraction: Intent? = null): ApiResponse {
private suspend fun executeApiRequest(
request: Request,
resultOfUserInteraction: Intent? = null
): ApiResponse {
d { "executeRequest($request) called" }
val result =
withContext(Dispatchers.Main) {
@ -141,7 +146,11 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
}
private suspend fun parseResult(request: Request, result: Intent): ApiResponse {
return when (result.getIntExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_ERROR)) {
return when (result.getIntExtra(
SshAuthenticationApi.EXTRA_RESULT_CODE,
SshAuthenticationApi.RESULT_CODE_ERROR
)
) {
SshAuthenticationApi.RESULT_CODE_SUCCESS -> {
ApiResponse.Success(
when (request) {
@ -153,20 +162,27 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
)
}
SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
val pendingIntent: PendingIntent = result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT)!!
val pendingIntent: PendingIntent =
result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT)!!
val resultOfUserInteraction: Intent =
withContext(Dispatchers.Main) {
suspendCoroutine { cont ->
activity.stashedCont = cont
activity.continueAfterUserInteraction.launch(IntentSenderRequest.Builder(pendingIntent).build())
activity.continueAfterUserInteraction.launch(
IntentSenderRequest.Builder(pendingIntent).build()
)
}
}
executeApiRequest(request, resultOfUserInteraction)
}
else -> {
val error = result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR)
val error =
result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR)
val exception =
UserAuthException(DisconnectReason.UNKNOWN, "Request ${request::class.simpleName} failed: ${error?.message}")
UserAuthException(
DisconnectReason.UNKNOWN,
"Request ${request::class.simpleName} failed: ${error?.message}"
)
when (error?.error) {
SshAuthenticationApiError.NO_AUTH_KEY, SshAuthenticationApiError.NO_SUCH_KEY ->
ApiResponse.NoSuchKey(exception)
@ -181,7 +197,9 @@ class OpenKeychainKeyProvider private constructor(val activity: ContinuationCont
privateKey =
object : OpenKeychainPrivateKey {
override suspend fun sign(challenge: ByteArray, hashAlgorithm: Int) =
when (val signingResponse = executeApiRequest(SigningRequest(challenge, keyId, hashAlgorithm))) {
when (val signingResponse =
executeApiRequest(SigningRequest(challenge, keyId, hashAlgorithm))
) {
is ApiResponse.Success -> (signingResponse.response as SigningResponse).signature
is ApiResponse.GeneralError -> throw signingResponse.exception
is ApiResponse.NoSuchKey -> throw signingResponse.exception

View file

@ -28,7 +28,8 @@ class OpenKeychainWrappedKeyAlgorithmFactory(private val factory: Factory.Named<
override fun create() = OpenKeychainWrappedKeyAlgorithm(factory.create())
}
class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) : KeyAlgorithm by keyAlgorithm {
class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) :
KeyAlgorithm by keyAlgorithm {
private val hashAlgorithm =
when (keyAlgorithm.keyAlgorithm) {
@ -39,11 +40,14 @@ class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) :
else -> SshAuthenticationApi.SHA512
}
override fun newSignature() = OpenKeychainWrappedSignature(keyAlgorithm.newSignature(), hashAlgorithm)
override fun newSignature() =
OpenKeychainWrappedSignature(keyAlgorithm.newSignature(), hashAlgorithm)
}
class OpenKeychainWrappedSignature(private val wrappedSignature: Signature, private val hashAlgorithm: Int) :
Signature by wrappedSignature {
class OpenKeychainWrappedSignature(
private val wrappedSignature: Signature,
private val hashAlgorithm: Int
) : Signature by wrappedSignature {
private val data = ByteArrayOutputStream()

View file

@ -115,7 +115,8 @@ object SshKey {
private var type: Type?
get() = Type.fromValue(context.sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_KEY_TYPE))
set(value) = context.sharedPrefs.edit { putString(PreferenceKeys.GIT_REMOTE_KEY_TYPE, value?.value) }
set(value) =
context.sharedPrefs.edit { putString(PreferenceKeys.GIT_REMOTE_KEY_TYPE, value?.value) }
private val isStrongBoxSupported by lazy(LazyThreadSafetyMode.NONE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
@ -138,13 +139,20 @@ object SshKey {
}
}
enum class Algorithm(val algorithm: String, val applyToSpec: KeyGenParameterSpec.Builder.() -> Unit) {
enum class Algorithm(
val algorithm: String,
val applyToSpec: KeyGenParameterSpec.Builder.() -> Unit
) {
Rsa(
KeyProperties.KEY_ALGORITHM_RSA,
{
setKeySize(3072)
setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
setDigests(KeyProperties.DIGEST_SHA1, KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
setDigests(
KeyProperties.DIGEST_SHA1,
KeyProperties.DIGEST_SHA256,
KeyProperties.DIGEST_SHA512
)
}
),
Ecdsa(
@ -163,7 +171,9 @@ object SshKey {
private fun delete() {
androidKeystore.deleteEntry(KEYSTORE_ALIAS)
// Remove Tink key set used by AndroidX's EncryptedFile.
context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE).edit { clear() }
context.getSharedPreferences(ANDROIDX_SECURITY_KEYSET_PREF_NAME, Context.MODE_PRIVATE).edit {
clear()
}
if (privateKeyFile.isFile) {
privateKeyFile.delete()
}
@ -177,7 +187,8 @@ object SshKey {
fun import(uri: Uri) {
// First check whether the content at uri is likely an SSH private key.
val fileSize =
context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use { cursor ->
context.contentResolver.query(uri, arrayOf(OpenableColumns.SIZE), null, null, null)?.use {
cursor ->
// Cursor returns only a single row.
cursor.moveToFirst()
cursor.getInt(0)
@ -186,7 +197,9 @@ object SshKey {
// We assume that an SSH key's ideal size is > 0 bytes && < 100 kilobytes.
if (fileSize > 100_000 || fileSize == 0)
throw IllegalArgumentException(context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message))
throw IllegalArgumentException(
context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message)
)
val sshKeyInputStream =
context.contentResolver.openInputStream(uri)
@ -199,7 +212,9 @@ object SshKey {
!Regex("BEGIN .* PRIVATE KEY").containsMatchIn(lines.first()) ||
!Regex("END .* PRIVATE KEY").containsMatchIn(lines.last())
)
throw IllegalArgumentException(context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message))
throw IllegalArgumentException(
context.getString(R.string.ssh_key_import_error_not_an_ssh_key_message)
)
// At this point, we are reasonably confident that we have actually been provided a private
// key and delete the old key.
@ -249,7 +264,9 @@ object SshKey {
val encryptedPrivateKeyFile = getOrCreateWrappedPrivateKeyFile(requireAuthentication)
// Generate the ed25519 key pair and encrypt the private key.
val keyPair = net.i2p.crypto.eddsa.KeyPairGenerator().generateKeyPair()
encryptedPrivateKeyFile.openFileOutput().use { os -> os.write((keyPair.private as EdDSAPrivateKey).seed) }
encryptedPrivateKeyFile.openFileOutput().use { os ->
os.write((keyPair.private as EdDSAPrivateKey).seed)
}
// Write public key in SSH format to .ssh_key.pub.
publicKeyFile.writeText(toSshPublicKey(keyPair.public))
@ -288,7 +305,8 @@ object SshKey {
fun provide(client: SSHClient, passphraseFinder: InteractivePasswordFinder): KeyProvider? =
when (type) {
Type.LegacyGenerated, Type.Imported -> client.loadKeys(privateKeyFile.absolutePath, passphraseFinder)
Type.LegacyGenerated, Type.Imported ->
client.loadKeys(privateKeyFile.absolutePath, passphraseFinder)
Type.KeystoreNative -> KeystoreNativeKeyProvider
Type.KeystoreWrappedEd25519 -> KeystoreWrappedEd25519KeyProvider
null -> null
@ -305,7 +323,10 @@ object SshKey {
override fun getPrivate(): PrivateKey =
runCatching { androidKeystore.sshPrivateKey!! }.getOrElse { error ->
e(error)
throw IOException("Failed to access private key '$KEYSTORE_ALIAS' from Android Keystore", error)
throw IOException(
"Failed to access private key '$KEYSTORE_ALIAS' from Android Keystore",
error
)
}
override fun getType(): KeyType = KeyType.fromKey(public)
@ -326,7 +347,9 @@ object SshKey {
// for `requireAuthentication` is not used as the key already exists at this point.
val encryptedPrivateKeyFile = runBlocking { getOrCreateWrappedPrivateKeyFile(false) }
val rawPrivateKey = encryptedPrivateKeyFile.openFileInput().use { it.readBytes() }
EdDSAPrivateKey(EdDSAPrivateKeySpec(rawPrivateKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC))
EdDSAPrivateKey(
EdDSAPrivateKeySpec(rawPrivateKey, EdDSANamedCurveTable.ED_25519_CURVE_SPEC)
)
}
.getOrElse { error ->
e(error)

View file

@ -37,7 +37,8 @@ fun setUpBouncyCastleForSshj() {
// Replace the Android BC provider with the Java BouncyCastle provider since the former does
// not include all the required algorithms.
// Note: This may affect crypto operations in other parts of the application.
val bcIndex = Security.getProviders().indexOfFirst { it.name == BouncyCastleProvider.PROVIDER_NAME }
val bcIndex =
Security.getProviders().indexOfFirst { it.name == BouncyCastleProvider.PROVIDER_NAME }
if (bcIndex == -1) {
// No Android BC found, install Java BC at lowest priority.
Security.addProvider(BouncyCastleProvider())
@ -77,9 +78,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun trace(msg: String, t: Throwable?) = t(msg, t)
override fun trace(marker: Marker, msg: String) = trace(msg)
override fun trace(marker: Marker?, format: String, arg: Any?) = trace(format, arg)
override fun trace(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = trace(format, arg1, arg2)
override fun trace(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
trace(format, arg1, arg2)
override fun trace(marker: Marker?, format: String, vararg arguments: Any?) = trace(format, *arguments)
override fun trace(marker: Marker?, format: String, vararg arguments: Any?) =
trace(format, *arguments)
override fun trace(marker: Marker?, msg: String, t: Throwable?) = trace(msg, t)
@ -90,9 +93,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun debug(msg: String, t: Throwable?) = d(msg, t)
override fun debug(marker: Marker, msg: String) = debug(msg)
override fun debug(marker: Marker?, format: String, arg: Any?) = debug(format, arg)
override fun debug(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = debug(format, arg1, arg2)
override fun debug(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
debug(format, arg1, arg2)
override fun debug(marker: Marker?, format: String, vararg arguments: Any?) = debug(format, *arguments)
override fun debug(marker: Marker?, format: String, vararg arguments: Any?) =
debug(format, *arguments)
override fun debug(marker: Marker?, msg: String, t: Throwable?) = debug(msg, t)
@ -103,9 +108,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun info(msg: String, t: Throwable?) = i(msg, t)
override fun info(marker: Marker, msg: String) = info(msg)
override fun info(marker: Marker?, format: String, arg: Any?) = info(format, arg)
override fun info(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = info(format, arg1, arg2)
override fun info(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
info(format, arg1, arg2)
override fun info(marker: Marker?, format: String, vararg arguments: Any?) = info(format, *arguments)
override fun info(marker: Marker?, format: String, vararg arguments: Any?) =
info(format, *arguments)
override fun info(marker: Marker?, msg: String, t: Throwable?) = info(msg, t)
@ -116,9 +123,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun warn(msg: String, t: Throwable?) = w(msg, t)
override fun warn(marker: Marker, msg: String) = warn(msg)
override fun warn(marker: Marker?, format: String, arg: Any?) = warn(format, arg)
override fun warn(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = warn(format, arg1, arg2)
override fun warn(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
warn(format, arg1, arg2)
override fun warn(marker: Marker?, format: String, vararg arguments: Any?) = warn(format, *arguments)
override fun warn(marker: Marker?, format: String, vararg arguments: Any?) =
warn(format, *arguments)
override fun warn(marker: Marker?, msg: String, t: Throwable?) = warn(msg, t)
@ -129,9 +138,11 @@ private abstract class AbstractLogger(private val name: String) : Logger {
override fun error(msg: String, t: Throwable?) = e(msg, t)
override fun error(marker: Marker, msg: String) = error(msg)
override fun error(marker: Marker?, format: String, arg: Any?) = error(format, arg)
override fun error(marker: Marker?, format: String, arg1: Any?, arg2: Any?) = error(format, arg1, arg2)
override fun error(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
error(format, arg1, arg2)
override fun error(marker: Marker?, format: String, vararg arguments: Any?) = error(format, *arguments)
override fun error(marker: Marker?, format: String, vararg arguments: Any?) =
error(format, *arguments)
override fun error(marker: Marker?, msg: String, t: Throwable?) = error(msg, t)
}
@ -148,7 +159,8 @@ object TimberLoggerFactory : LoggerFactory {
// Replace slf4j's "{}" format string style with standard Java's "%s".
// The supposedly redundant escape on the } is not redundant.
@Suppress("RegExpRedundantEscape") private fun String.fix() = replace("""(?!<\\)\{\}""".toRegex(), "%s")
@Suppress("RegExpRedundantEscape")
private fun String.fix() = replace("""(?!<\\)\{\}""".toRegex(), "%s")
override fun t(message: String, t: Throwable?, vararg args: Any?) {
Timber.tag(name).v(t, message.fix(), *args)

View file

@ -52,7 +52,10 @@ abstract class InteractivePasswordFinder : PasswordFinder {
abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean)
final override fun reqPassword(resource: Resource<*>?): CharArray {
val password = runBlocking(Dispatchers.Main) { suspendCoroutine<String?> { cont -> askForPassword(cont, isRetry) } }
val password =
runBlocking(Dispatchers.Main) {
suspendCoroutine<String?> { cont -> askForPassword(cont, isRetry) }
}
isRetry = true
return password?.toCharArray() ?: throw SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER)
}
@ -60,11 +63,17 @@ abstract class InteractivePasswordFinder : PasswordFinder {
final override fun shouldRetry(resource: Resource<*>?) = true
}
class SshjSessionFactory(private val authMethod: SshAuthMethod, private val hostKeyFile: File) : SshSessionFactory() {
class SshjSessionFactory(private val authMethod: SshAuthMethod, private val hostKeyFile: File) :
SshSessionFactory() {
private var currentSession: SshjSession? = null
override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
override fun getSession(
uri: URIish,
credentialsProvider: CredentialsProvider?,
fs: FS?,
tms: Int
): RemoteSession {
return currentSession
?: SshjSession(uri, uri.user, authMethod, hostKeyFile).connect().also {
d { "New SSH connection created" }
@ -81,7 +90,9 @@ private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
if (!hostKeyFile.exists()) {
return HostKeyVerifier { _, _, key ->
val digest =
runCatching { SecurityUtils.getMessageDigest("SHA-256") }.getOrElse { e -> throw SSHRuntimeException(e) }
runCatching { SecurityUtils.getMessageDigest("SHA-256") }.getOrElse { e ->
throw SSHRuntimeException(e)
}
digest.update(PlainBuffer().putPublicKey(key).compactData)
val digestData = digest.digest()
val hostKeyEntry = "SHA256:${Base64.encodeToString(digestData, Base64.NO_WRAP)}"
@ -115,7 +126,9 @@ private class SshjSession(
val userPlusHost = "${uri.user}@${uri.host}"
val realUser = userPlusHost.substringBeforeLast('@')
val realHost = userPlusHost.substringAfterLast('@')
uri.setUser(realUser).setHost(realHost).also { d { "After fixup: user=${it.user}, host=${it.host}" } }
uri.setUser(realUser).setHost(realHost).also {
d { "After fixup: user=${it.user}, host=${it.host}" }
}
} else {
uri
}
@ -131,7 +144,8 @@ private class SshjSession(
ssh.auth(username, passwordAuth)
}
is SshAuthMethod.SshKey -> {
val pubkeyAuth = AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey)))
val pubkeyAuth =
AuthPublickey(SshKey.provide(ssh, CredentialFinder(authMethod.activity, AuthMode.SshKey)))
ssh.auth(username, pubkeyAuth, passwordAuth)
}
is SshAuthMethod.OpenKeychain -> {
@ -174,7 +188,8 @@ private class SshjSession(
}
}
private class SshjProcess(private val command: Session.Command, private val timeout: Long) : Process() {
private class SshjProcess(private val command: Session.Command, private val timeout: Long) :
Process() {
override fun waitFor(): Int {
command.join(timeout, TimeUnit.SECONDS)

View file

@ -42,7 +42,10 @@ object PasswordGenerator {
*/
fun setPrefs(ctx: Context, options: List<PasswordOption>, targetLength: Int): Boolean {
ctx.getSharedPreferences("PasswordGenerator", Context.MODE_PRIVATE).edit {
for (possibleOption in PasswordOption.values()) putBoolean(possibleOption.key, possibleOption in options)
for (possibleOption in PasswordOption.values()) putBoolean(
possibleOption.key,
possibleOption in options
)
putInt("length", targetLength)
}
return true
@ -82,7 +85,9 @@ object PasswordGenerator {
} else {
// The No* options are false, so the respective character category will be included.
when (option) {
PasswordOption.NoDigits, PasswordOption.NoUppercaseLetters, PasswordOption.NoLowercaseLetters -> {
PasswordOption.NoDigits,
PasswordOption.NoUppercaseLetters,
PasswordOption.NoLowercaseLetters -> {
numCharacterCategories++
}
PasswordOption.NoAmbiguousCharacters,
@ -98,7 +103,9 @@ object PasswordGenerator {
throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_no_chars_error))
}
if (length < numCharacterCategories) {
throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_length_too_short_error))
throw PasswordGeneratorException(
ctx.resources.getString(R.string.pwgen_length_too_short_error)
)
}
if (!(pwgenFlags hasFlag UPPERS) && !(pwgenFlags hasFlag LOWERS)) {
phonemes = false
@ -114,7 +121,9 @@ object PasswordGenerator {
var iterations = 0
do {
if (iterations++ > 1000)
throw PasswordGeneratorException(ctx.resources.getString(R.string.pwgen_max_iterations_exceeded))
throw PasswordGeneratorException(
ctx.resources.getString(R.string.pwgen_max_iterations_exceeded)
)
password =
if (phonemes) {
RandomPhonemesGenerator.generate(length, pwgenFlags)

View file

@ -36,7 +36,9 @@ object RandomPasswordGenerator {
var password = ""
while (password.length < targetLength) {
val candidate = bank.secureRandomCharacter()
if (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && candidate in PasswordGenerator.AMBIGUOUS_STR) {
if (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
candidate in PasswordGenerator.AMBIGUOUS_STR
) {
continue
}
password += candidate

View file

@ -100,7 +100,9 @@ object RandomPhonemesGenerator {
if (!candidate.flags.hasFlag(nextBasicType) ||
(isStartOfPart && candidate.flags hasFlag NOT_FIRST) ||
// Don't let a diphthong that starts with a vowel follow a vowel.
(previousFlags hasFlag VOWEL && candidate.flags hasFlag VOWEL && candidate.flags hasFlag DIPHTHONG) ||
(previousFlags hasFlag VOWEL &&
candidate.flags hasFlag VOWEL &&
candidate.flags hasFlag DIPHTHONG) ||
// Don't add multi-character candidates if we would go over the targetLength.
(password.length + candidate.length > targetLength) ||
(pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && candidate.isAmbiguous)
@ -129,11 +131,15 @@ object RandomPhonemesGenerator {
// Second part: Add digits and symbols with a certain probability (if requested) if
// they would not directly follow the first character in a pronounceable part.
if (!isStartOfPart && pwFlags hasFlag PasswordGenerator.DIGITS && secureRandomBiasedBoolean(30)) {
if (!isStartOfPart &&
pwFlags hasFlag PasswordGenerator.DIGITS &&
secureRandomBiasedBoolean(30)
) {
var randomDigit: Char
do {
randomDigit = secureRandomNumber(10).toString(10).first()
} while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && randomDigit in PasswordGenerator.AMBIGUOUS_STR)
} while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
randomDigit in PasswordGenerator.AMBIGUOUS_STR)
password += randomDigit
// Begin a new pronounceable part after every digit.
@ -143,11 +149,15 @@ object RandomPhonemesGenerator {
continue
}
if (!isStartOfPart && pwFlags hasFlag PasswordGenerator.SYMBOLS && secureRandomBiasedBoolean(20)) {
if (!isStartOfPart &&
pwFlags hasFlag PasswordGenerator.SYMBOLS &&
secureRandomBiasedBoolean(20)
) {
var randomSymbol: Char
do {
randomSymbol = PasswordGenerator.SYMBOLS_STR.secureRandomCharacter()
} while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS && randomSymbol in PasswordGenerator.AMBIGUOUS_STR)
} while (pwFlags hasFlag PasswordGenerator.NO_AMBIGUOUS &&
randomSymbol in PasswordGenerator.AMBIGUOUS_STR)
password += randomSymbol
// Continue the password generation as if nothing was added.
}
@ -157,8 +167,9 @@ object RandomPhonemesGenerator {
nextBasicType =
when {
candidate.flags.hasFlag(CONSONANT) -> VOWEL
previousFlags.hasFlag(VOWEL) || candidate.flags.hasFlag(DIPHTHONG) || secureRandomBiasedBoolean(60) ->
CONSONANT
previousFlags.hasFlag(VOWEL) ||
candidate.flags.hasFlag(DIPHTHONG) ||
secureRandomBiasedBoolean(60) -> CONSONANT
else -> VOWEL
}
previousFlags = candidate.flags

View file

@ -109,7 +109,9 @@ class PasswordBuilder(ctx: Context) {
}
else candidate
CapsType.TitleCase ->
candidate.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
candidate.replaceFirstChar {
if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString()
}
CapsType.lowercase -> candidate.lowercase(Locale.getDefault())
CapsType.As_iS -> candidate
}

View file

@ -30,7 +30,10 @@ class XkpwdDictionary(context: Context) {
context.resources.openRawResource(R.raw.xkpwdict).bufferedReader().readLines()
}
words = lines.asSequence().map { it.trim() }.filter { it.isNotEmpty() && !it.contains(' ') }.groupBy { it.length }
words =
lines.asSequence().map { it.trim() }.filter { it.isNotEmpty() && !it.contains(' ') }.groupBy {
it.length
}
}
companion object {

View file

@ -140,7 +140,10 @@ class ClipboardService : Service() {
}
@RequiresApi(Build.VERSION_CODES.N)
private fun createNotificationApi24(pendingIntent: PendingIntent, clearTimeMs: Long): Notification {
private fun createNotificationApi24(
pendingIntent: PendingIntent,
clearTimeMs: Long
): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.app_name))
.setContentText(getString(R.string.tap_clear_clipboard))
@ -157,7 +160,11 @@ class ClipboardService : Service() {
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel =
NotificationChannel(CHANNEL_ID, getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW)
NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService<NotificationManager>()
if (manager != null) {
manager.createNotificationChannel(serviceChannel)

View file

@ -60,7 +60,11 @@ class OreoAutofillService : AutofillService() {
cachePublicSuffixList(applicationContext)
}
override fun onFillRequest(request: FillRequest, cancellationSignal: CancellationSignal, callback: FillCallback) {
override fun onFillRequest(
request: FillRequest,
cancellationSignal: CancellationSignal,
callback: FillCallback
) {
val structure =
request.fillContexts.lastOrNull()?.structure
?: run {
@ -93,7 +97,8 @@ class OreoAutofillService : AutofillService() {
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
Api30AutofillResponseBuilder(formToFill).fillCredentials(this, request.inlineSuggestionsRequest, callback)
Api30AutofillResponseBuilder(formToFill)
.fillCredentials(this, request.inlineSuggestionsRequest, callback)
} else {
AutofillResponseBuilder(formToFill).fillCredentials(this, callback)
}
@ -148,11 +153,13 @@ class OreoAutofillService : AutofillService() {
}
}
fun Context.getDefaultUsername() = sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
fun Context.getDefaultUsername() =
sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
fun Context.getCustomSuffixes(): Sequence<String> {
return sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)?.splitToSequence('\n')?.filter {
it.isNotBlank() && it.first() != '.' && it.last() != '.'
}
return sharedPrefs
.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
?.splitToSequence('\n')
?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
?: emptySequence()
}

View file

@ -137,7 +137,11 @@ class PasswordExportService : Service() {
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel =
NotificationChannel(CHANNEL_ID, getString(R.string.app_name), NotificationManager.IMPORTANCE_LOW)
NotificationChannel(
CHANNEL_ID,
getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService<NotificationManager>()
if (manager != null) {
manager.createNotificationChannel(serviceChannel)

View file

@ -25,7 +25,8 @@ enum class Protocol(val pref: String) {
private val map = values().associateBy(Protocol::pref)
fun fromString(type: String?): Protocol {
return map[type ?: return Ssh] ?: throw IllegalArgumentException("$type is not a valid Protocol")
return map[type ?: return Ssh]
?: throw IllegalArgumentException("$type is not a valid Protocol")
}
}
}
@ -41,7 +42,8 @@ enum class AuthMode(val pref: String) {
private val map = values().associateBy(AuthMode::pref)
fun fromString(type: String?): AuthMode {
return map[type ?: return SshKey] ?: throw IllegalArgumentException("$type is not a valid AuthMode")
return map[type ?: return SshKey]
?: throw IllegalArgumentException("$type is not a valid AuthMode")
}
}
}
@ -50,12 +52,18 @@ object GitSettings {
private const val DEFAULT_BRANCH = "master"
private val settings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.sharedPrefs }
private val settings by lazy(LazyThreadSafetyMode.PUBLICATION) {
Application.instance.sharedPrefs
}
private val encryptedSettings by lazy(LazyThreadSafetyMode.PUBLICATION) {
Application.instance.getEncryptedGitPrefs()
}
private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedProxyPrefs() }
private val hostKeyPath by lazy(LazyThreadSafetyMode.NONE) { "${Application.instance.filesDir}/.host_key" }
private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) {
Application.instance.getEncryptedProxyPrefs()
}
private val hostKeyPath by lazy(LazyThreadSafetyMode.NONE) {
"${Application.instance.filesDir}/.host_key"
}
var authMode
get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))

View file

@ -60,7 +60,8 @@ private fun migrateToGitUrlBasedConfig(sharedPrefs: SharedPreferences) {
val url =
when {
urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme
urlWithFreeEntryScheme.startsWith("http://") -> urlWithFreeEntryScheme.replaceFirst("http", "https")
urlWithFreeEntryScheme.startsWith("http://") ->
urlWithFreeEntryScheme.replaceFirst("http", "https")
else -> "https://$urlWithFreeEntryScheme"
}
runCatching { if (URI(url).rawAuthority != null) url else null }.get()
@ -96,7 +97,10 @@ private fun migrateToHideAll(sharedPrefs: SharedPreferences) {
private fun migrateToSshKey(context: Context, sharedPrefs: SharedPreferences) {
val privateKeyFile = File(context.filesDir, ".ssh_key")
if (sharedPrefs.contains(PreferenceKeys.USE_GENERATED_KEY) && !SshKey.exists && privateKeyFile.exists()) {
if (sharedPrefs.contains(PreferenceKeys.USE_GENERATED_KEY) &&
!SshKey.exists &&
privateKeyFile.exists()
) {
// Currently uses a private key imported or generated with an old version of Password Store.
// Generated keys come with a public key which the user should still be able to view after
// the migration (not possible for regular imported keys), hence the special case.

View file

@ -18,10 +18,15 @@ enum class PasswordSortOrder(val comparator: java.util.Comparator<PasswordItem>)
(p1.type + p1.name).compareTo(p2.type + p2.name, ignoreCase = true)
}
),
INDEPENDENT(Comparator { p1: PasswordItem, p2: PasswordItem -> p1.name.compareTo(p2.name, ignoreCase = true) }),
INDEPENDENT(
Comparator { p1: PasswordItem, p2: PasswordItem ->
p1.name.compareTo(p2.name, ignoreCase = true)
}
),
RECENTLY_USED(
Comparator { p1: PasswordItem, p2: PasswordItem ->
val recentHistory = Application.instance.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
val recentHistory =
Application.instance.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
val timeP1 = recentHistory.getString(p1.file.absolutePath.base64())
val timeP2 = recentHistory.getString(p2.file.absolutePath.base64())
when {

View file

@ -44,7 +44,8 @@ class UriTotpFinder @Inject constructor() : TotpFinder {
override fun findAlgorithm(content: String): String {
content.split("\n".toRegex()).forEach { line ->
if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("algorithm") != null) {
if (line.startsWith(TOTP_FIELDS[0]) && Uri.parse(line).getQueryParameter("algorithm") != null
) {
return Uri.parse(line).getQueryParameter("algorithm")!!
}
}

View file

@ -75,8 +75,14 @@ private fun PasswordItem.Companion.makeComparator(
PasswordSortOrder.FILE_FIRST -> compareByDescending { it.type }
PasswordSortOrder.RECENTLY_USED -> PasswordSortOrder.RECENTLY_USED.comparator
}
.then(compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getIdentifierFor(it.file) })
.then(compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getUsernameFor(it.file) })
.then(
compareBy(nullsLast(CaseInsensitiveComparator)) {
directoryStructure.getIdentifierFor(it.file)
}
)
.then(
compareBy(nullsLast(CaseInsensitiveComparator)) { directoryStructure.getUsernameFor(it.file) }
)
}
val PasswordItem.stableId: String
@ -179,7 +185,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
.mapLatest { searchAction ->
val listResultFlow =
when (searchAction.searchMode) {
SearchMode.RecursivelyInSubdirectories -> listFilesRecursively(searchAction.baseDirectory)
SearchMode.RecursivelyInSubdirectories ->
listFilesRecursively(searchAction.baseDirectory)
SearchMode.InCurrentDirectoryOnly -> listFiles(searchAction.baseDirectory)
}
val prefilteredResultFlow =
@ -188,9 +195,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
ListMode.DirectoriesOnly -> listResultFlow.filter { it.isDirectory }
ListMode.AllEntries -> listResultFlow
}
val filterModeToUse = if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode
val passwordList =
when (filterModeToUse) {
when (if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode) {
FilterMode.NoFilter -> {
prefilteredResultFlow.map { it.toPasswordItem() }.toList().sortedWith(itemComparator)
}
@ -201,7 +207,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
val regex = generateStrictDomainRegex(searchAction.filter)
if (regex != null) {
prefilteredResultFlow
.filter { absoluteFile -> regex.containsMatchIn(absoluteFile.relativeTo(root).path) }
.filter { absoluteFile ->
regex.containsMatchIn(absoluteFile.relativeTo(root).path)
}
.map { it.toPasswordItem() }
.toList()
.sortedWith(itemComparator)
@ -218,7 +226,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
.filter { it.first > 0 }
.toList()
.sortedWith(
compareByDescending<Pair<Int, PasswordItem>> { it.first }.thenBy(itemComparator) { it.second }
compareByDescending<Pair<Int, PasswordItem>> { it.first }.thenBy(itemComparator) {
it.second
}
)
.map { it.second }
}
@ -387,7 +397,9 @@ open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
addObserver(
object : SelectionTracker.SelectionObserver<String>() {
override fun onSelectionChanged() {
this@SearchableRepositoryAdapter.onSelectionChangedListener?.invoke(requireSelectionTracker().selection)
this@SearchableRepositoryAdapter.onSelectionChangedListener?.invoke(
requireSelectionTracker().selection
)
}
}
)
@ -395,15 +407,23 @@ open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
}
private var onItemClickedListener: ((holder: T, item: PasswordItem) -> Unit)? = null
open fun onItemClicked(listener: (holder: T, item: PasswordItem) -> Unit): SearchableRepositoryAdapter<T> {
check(onItemClickedListener == null) { "Only a single listener can be registered for onItemClicked" }
open fun onItemClicked(
listener: (holder: T, item: PasswordItem) -> Unit
): SearchableRepositoryAdapter<T> {
check(onItemClickedListener == null) {
"Only a single listener can be registered for onItemClicked"
}
onItemClickedListener = listener
return this
}
private var onSelectionChangedListener: ((selection: Selection<String>) -> Unit)? = null
open fun onSelectionChanged(listener: (selection: Selection<String>) -> Unit): SearchableRepositoryAdapter<T> {
check(onSelectionChangedListener == null) { "Only a single listener can be registered for onSelectionChanged" }
open fun onSelectionChanged(
listener: (selection: Selection<String>) -> Unit
): SearchableRepositoryAdapter<T> {
check(onSelectionChangedListener == null) {
"Only a single listener can be registered for onSelectionChanged"
}
onSelectionChangedListener = listener
return this
}

View file

@ -96,4 +96,3 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -77,7 +77,12 @@ class AutofillSmsActivity : AppCompatActivity() {
fun makeFillOtpFromSmsIntentSender(context: Context): IntentSender {
val intent = Intent(context, AutofillSmsActivity::class.java)
return PendingIntent.getActivity(context, fillOtpFromSmsRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT)
return PendingIntent.getActivity(
context,
fillOtpFromSmsRequestCode++,
intent,
PendingIntent.FLAG_CANCEL_CURRENT
)
.intentSender
}
}
@ -122,15 +127,17 @@ class AutofillSmsActivity : AppCompatActivity() {
private suspend fun waitForSms() {
val smsClient = SmsCodeRetriever.getAutofillClient(this@AutofillSmsActivity)
runCatching { withContext(Dispatchers.IO) { smsClient.startSmsCodeRetriever().suspendableAwait() } }.onFailure { e
->
if (e is ResolvableApiException) {
e.startResolutionForResult(this@AutofillSmsActivity, 1)
} else {
e(e)
withContext(Dispatchers.Main) { finish() }
}
runCatching {
withContext(Dispatchers.IO) { smsClient.startSmsCodeRetriever().suspendableAwait() }
}
.onFailure { e ->
if (e is ResolvableApiException) {
e.startResolutionForResult(this@AutofillSmsActivity, 1)
} else {
e(e)
withContext(Dispatchers.Main) { finish() }
}
}
}
private val smsCodeRetrievedReceiver =
@ -144,7 +151,10 @@ class AutofillSmsActivity : AppCompatActivity() {
clientState,
AutofillAction.FillOtpFromSms
)
setResult(RESULT_OK, Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) })
setResult(
RESULT_OK,
Intent().apply { putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) }
)
finish()
}
}

View file

@ -41,7 +41,8 @@ public sealed class FormOrigin(public open val identifier: String) {
when (this) {
is Web -> identifier
is App -> {
val info = context.packageManager.getApplicationInfo(identifier, PackageManager.GET_META_DATA)
val info =
context.packageManager.getApplicationInfo(identifier, PackageManager.GET_META_DATA)
val label = context.packageManager.getApplicationLabel(info)
if (untrusted) "$label" else "$label"
}
@ -174,7 +175,10 @@ private class AutofillFormParser(
// the single origin among the detected fillable or saveable fields. If this origin
// is null, but we encountered web origins elsewhere in the AssistStructure, the
// situation is uncertain and Autofill should not be offered.
webOriginToFormOrigin(context, scenario.allFields.map { it.webOrigin }.toSet().singleOrNull() ?: return null)
webOriginToFormOrigin(
context,
scenario.allFields.map { it.webOrigin }.toSet().singleOrNull() ?: return null
)
}
}
}
@ -204,7 +208,12 @@ private constructor(
): FillableForm? {
val form = AutofillFormParser(context, structure, isManualRequest, customSuffixes)
if (form.formOrigin == null || form.scenario == null) return null
return FillableForm(form.formOrigin, form.scenario.map { it.autofillId }, form.ignoredIds, form.saveFlags)
return FillableForm(
form.formOrigin,
form.scenario.map { it.autofillId },
form.ignoredIds,
form.saveFlags
)
}
}

View file

@ -49,14 +49,19 @@ public fun computeCertificatesHash(context: Context, appPackage: String): String
// hashes comparable between versions and hence default to using the deprecated API.
@SuppressLint("PackageManagerGetSignatures")
@Suppress("DEPRECATION")
val signaturesOld = context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNATURES).signatures
val signaturesOld =
context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNATURES).signatures
val stableHashOld = stableHash(signaturesOld.map { it.toByteArray() })
if (Build.VERSION.SDK_INT >= 28) {
val info = context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNING_CERTIFICATES)
val signaturesNew = info.signingInfo.signingCertificateHistory ?: info.signingInfo.apkContentsSigners
val info =
context.packageManager.getPackageInfo(appPackage, PackageManager.GET_SIGNING_CERTIFICATES)
val signaturesNew =
info.signingInfo.signingCertificateHistory ?: info.signingInfo.apkContentsSigners
val stableHashNew = stableHash(signaturesNew.map { it.toByteArray() })
if (stableHashNew != stableHashOld)
tag("CertificatesHash").e { "Mismatch between old and new hash: $stableHashNew != $stableHashOld" }
tag("CertificatesHash").e {
"Mismatch between old and new hash: $stableHashNew != $stableHashOld"
}
}
return stableHashOld
}
@ -106,7 +111,10 @@ private fun visitViewNodes(structure: AssistStructure, block: (AssistStructure.V
}
}
private fun visitViewNode(node: AssistStructure.ViewNode, block: (AssistStructure.ViewNode) -> Unit) {
private fun visitViewNode(
node: AssistStructure.ViewNode,
block: (AssistStructure.ViewNode) -> Unit
) {
block(node)
for (i in 0 until node.childCount) {
visitViewNode(node.getChildAt(i), block)
@ -114,7 +122,9 @@ private fun visitViewNode(node: AssistStructure.ViewNode, block: (AssistStructur
}
@RequiresApi(Build.VERSION_CODES.O)
internal fun AssistStructure.findNodeByAutofillId(autofillId: AutofillId): AssistStructure.ViewNode? {
internal fun AssistStructure.findNodeByAutofillId(
autofillId: AutofillId
): AssistStructure.ViewNode? {
var node: AssistStructure.ViewNode? = null
visitViewNodes(this) { if (it.autofillId == autofillId) node = it }
return node

View file

@ -56,9 +56,15 @@ public sealed class AutofillScenario<out T : Any> {
username = clientState.getParcelable(BUNDLE_KEY_USERNAME_ID)
fillUsername = clientState.getBoolean(BUNDLE_KEY_FILL_USERNAME)
otp = clientState.getParcelable(BUNDLE_KEY_OTP_ID)
currentPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_CURRENT_PASSWORD_IDS) ?: emptyList())
newPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_NEW_PASSWORD_IDS) ?: emptyList())
genericPassword.addAll(clientState.getParcelableArrayList(BUNDLE_KEY_GENERIC_PASSWORD_IDS) ?: emptyList())
currentPassword.addAll(
clientState.getParcelableArrayList(BUNDLE_KEY_CURRENT_PASSWORD_IDS) ?: emptyList()
)
newPassword.addAll(
clientState.getParcelableArrayList(BUNDLE_KEY_NEW_PASSWORD_IDS) ?: emptyList()
)
genericPassword.addAll(
clientState.getParcelableArrayList(BUNDLE_KEY_GENERIC_PASSWORD_IDS) ?: emptyList()
)
}
.build()
} catch (e: Throwable) {
@ -227,7 +233,9 @@ public fun Dataset.Builder.fillWith(
}
}
internal inline fun <T : Any, S : Any> AutofillScenario<T>.map(transform: (T) -> S): AutofillScenario<S> {
internal inline fun <T : Any, S : Any> AutofillScenario<T>.map(
transform: (T) -> S
): AutofillScenario<S> {
val builder = AutofillScenario.Builder<S>()
builder.username = username?.let(transform)
builder.fillUsername = fillUsername
@ -253,7 +261,10 @@ internal fun AutofillScenario<AutofillId>.toBundle(): Bundle =
putParcelable(AutofillScenario.BUNDLE_KEY_USERNAME_ID, username)
putBoolean(AutofillScenario.BUNDLE_KEY_FILL_USERNAME, fillUsername)
putParcelable(AutofillScenario.BUNDLE_KEY_OTP_ID, otp)
putParcelableArrayList(AutofillScenario.BUNDLE_KEY_CURRENT_PASSWORD_IDS, ArrayList(currentPassword))
putParcelableArrayList(
AutofillScenario.BUNDLE_KEY_CURRENT_PASSWORD_IDS,
ArrayList(currentPassword)
)
putParcelableArrayList(AutofillScenario.BUNDLE_KEY_NEW_PASSWORD_IDS, ArrayList(newPassword))
}
}
@ -262,7 +273,10 @@ internal fun AutofillScenario<AutofillId>.toBundle(): Bundle =
putParcelable(AutofillScenario.BUNDLE_KEY_USERNAME_ID, username)
putBoolean(AutofillScenario.BUNDLE_KEY_FILL_USERNAME, fillUsername)
putParcelable(AutofillScenario.BUNDLE_KEY_OTP_ID, otp)
putParcelableArrayList(AutofillScenario.BUNDLE_KEY_GENERIC_PASSWORD_IDS, ArrayList(genericPassword))
putParcelableArrayList(
AutofillScenario.BUNDLE_KEY_GENERIC_PASSWORD_IDS,
ArrayList(genericPassword)
)
}
}
}

View file

@ -9,11 +9,14 @@ import androidx.annotation.RequiresApi
import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Certain
import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Likely
private inline fun <T> Pair<T, T>.all(predicate: T.() -> Boolean) = predicate(first) && predicate(second)
private inline fun <T> Pair<T, T>.all(predicate: T.() -> Boolean) =
predicate(first) && predicate(second)
private inline fun <T> Pair<T, T>.any(predicate: T.() -> Boolean) = predicate(first) || predicate(second)
private inline fun <T> Pair<T, T>.any(predicate: T.() -> Boolean) =
predicate(first) || predicate(second)
private inline fun <T> Pair<T, T>.none(predicate: T.() -> Boolean) = !predicate(first) && !predicate(second)
private inline fun <T> Pair<T, T>.none(predicate: T.() -> Boolean) =
!predicate(first) && !predicate(second)
/**
* The strategy used to detect [AutofillScenario] s; expressed using the DSL implemented in
@ -32,7 +35,8 @@ internal val autofillStrategy = strategy {
}
currentPassword(optional = true) {
takeSingle { alreadyMatched ->
val adjacentToNewPasswords = directlyPrecedes(alreadyMatched) || directlyFollows(alreadyMatched)
val adjacentToNewPasswords =
directlyPrecedes(alreadyMatched) || directlyFollows(alreadyMatched)
// The Autofill framework has not hint that applies to current passwords only.
// In this scenario, we have already matched fields a pair of fields with a specific
// new password hint, so we take a generic Autofill password hint to mean a current
@ -109,7 +113,9 @@ internal val autofillStrategy = strategy {
rule(applyInSingleOriginMode = true) {
newPassword { takeSingle { hasHintNewPassword && isFocused } }
username(optional = true) {
takeSingle { alreadyMatched -> usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) }
takeSingle { alreadyMatched ->
usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull())
}
}
}
@ -119,7 +125,9 @@ internal val autofillStrategy = strategy {
rule(applyInSingleOriginMode = true) {
currentPassword { takeSingle { hasAutocompleteHintCurrentPassword && isFocused } }
username(optional = true) {
takeSingle { alreadyMatched -> usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) }
takeSingle { alreadyMatched ->
usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull())
}
}
}
@ -129,7 +137,9 @@ internal val autofillStrategy = strategy {
rule(applyInSingleOriginMode = true) {
genericPassword { takeSingle { passwordCertainty >= Likely && isFocused } }
username(optional = true) {
takeSingle { alreadyMatched -> usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) }
takeSingle { alreadyMatched ->
usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull())
}
}
}
@ -139,12 +149,16 @@ internal val autofillStrategy = strategy {
rule {
username { takeSingle { hasHintUsername && isFocused } }
currentPassword(matchHidden = true) {
takeSingle { alreadyMatched -> directlyFollows(alreadyMatched.singleOrNull()) && couldBeTwoStepHiddenPassword }
takeSingle { alreadyMatched ->
directlyFollows(alreadyMatched.singleOrNull()) && couldBeTwoStepHiddenPassword
}
}
}
// Match a single focused OTP field.
rule(applyInSingleOriginMode = true) { otp { takeSingle { otpCertainty >= Likely && isFocused } } }
rule(applyInSingleOriginMode = true) {
otp { takeSingle { otpCertainty >= Likely && isFocused } }
}
// Match a single focused username field without a password field.
rule(applyInSingleOriginMode = true) {
@ -162,7 +176,9 @@ internal val autofillStrategy = strategy {
// This rule can apply in single origin mode since even though the password field may not be
// focused at the time the rule runs, the fill suggestion will only show if it ever receives
// focus.
rule(applyInSingleOriginMode = true) { currentPassword { takeSingle { hasAutocompleteHintCurrentPassword } } }
rule(applyInSingleOriginMode = true) {
currentPassword { takeSingle { hasAutocompleteHintCurrentPassword } }
}
// See above.
rule(applyInSingleOriginMode = true) { genericPassword { takeSingle { true } } }
@ -171,10 +187,14 @@ internal val autofillStrategy = strategy {
rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) {
genericPassword { takeSingle { isFocused } }
username(optional = true) {
takeSingle { alreadyMatched -> usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull()) }
takeSingle { alreadyMatched ->
usernameCertainty >= Likely && directlyPrecedes(alreadyMatched.singleOrNull())
}
}
}
// Match any focused username field on manual request.
rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) { username { takeSingle { isFocused } } }
rule(applyInSingleOriginMode = true, applyOnManualRequestOnly = true) {
username { takeSingle { isFocused } }
}
}

View file

@ -20,28 +20,41 @@ internal interface FieldMatcher {
class Builder {
private var takeSingle: (FormField.(List<FormField>) -> Boolean)? = null
private val tieBreakersSingle: MutableList<FormField.(List<FormField>) -> Boolean> = mutableListOf()
private val tieBreakersSingle: MutableList<FormField.(List<FormField>) -> Boolean> =
mutableListOf()
private var takePair: (Pair<FormField, FormField>.(List<FormField>) -> Boolean)? = null
private var tieBreakersPair: MutableList<Pair<FormField, FormField>.(List<FormField>) -> Boolean> = mutableListOf()
private var tieBreakersPair:
MutableList<Pair<FormField, FormField>.(List<FormField>) -> Boolean> =
mutableListOf()
fun takeSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean = { true }) {
check(takeSingle == null && takePair == null) { "Every block can only have at most one take{Single,Pair} block" }
check(takeSingle == null && takePair == null) {
"Every block can only have at most one take{Single,Pair} block"
}
takeSingle = block
}
fun breakTieOnSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean) {
check(takeSingle != null) { "Every block needs a takeSingle block before a breakTieOnSingle block" }
check(takeSingle != null) {
"Every block needs a takeSingle block before a breakTieOnSingle block"
}
check(takePair == null) { "takePair cannot be mixed with breakTieOnSingle" }
tieBreakersSingle.add(block)
}
fun takePair(block: Pair<FormField, FormField>.(alreadyMatched: List<FormField>) -> Boolean = { true }) {
check(takeSingle == null && takePair == null) { "Every block can only have at most one take{Single,Pair} block" }
fun takePair(
block: Pair<FormField, FormField>.(alreadyMatched: List<FormField>) -> Boolean = { true }
) {
check(takeSingle == null && takePair == null) {
"Every block can only have at most one take{Single,Pair} block"
}
takePair = block
}
fun breakTieOnPair(block: Pair<FormField, FormField>.(alreadyMatched: List<FormField>) -> Boolean) {
fun breakTieOnPair(
block: Pair<FormField, FormField>.(alreadyMatched: List<FormField>) -> Boolean
) {
check(takePair != null) { "Every block needs a takePair block before a breakTieOnPair block" }
check(takeSingle == null) { "takeSingle cannot be mixed with breakTieOnPair" }
tieBreakersPair.add(block)
@ -69,7 +82,8 @@ internal class SingleFieldMatcher(
class Builder {
private var takeSingle: (FormField.(List<FormField>) -> Boolean)? = null
private val tieBreakersSingle: MutableList<FormField.(List<FormField>) -> Boolean> = mutableListOf()
private val tieBreakersSingle: MutableList<FormField.(List<FormField>) -> Boolean> =
mutableListOf()
fun takeSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean = { true }) {
check(takeSingle == null) { "Every block can only have at most one takeSingle block" }
@ -77,7 +91,9 @@ internal class SingleFieldMatcher(
}
fun breakTieOnSingle(block: FormField.(alreadyMatched: List<FormField>) -> Boolean) {
check(takeSingle != null) { "Every block needs a takeSingle block before a breakTieOnSingle block" }
check(takeSingle != null) {
"Every block needs a takeSingle block before a breakTieOnSingle block"
}
tieBreakersSingle.add(block)
}
@ -180,7 +196,10 @@ private constructor(
}
@AutofillDsl
class Builder(private val applyInSingleOriginMode: Boolean, private val applyOnManualRequestOnly: Boolean) {
class Builder(
private val applyInSingleOriginMode: Boolean,
private val applyOnManualRequestOnly: Boolean
) {
companion object {
@ -286,9 +305,13 @@ private constructor(
"Rules with applyInSingleOriginMode set to true must not fill into hidden fields"
}
}
return AutofillRule(matchers, applyInSingleOriginMode, applyOnManualRequestOnly, name ?: "Rule #$ruleId").also {
ruleId++
}
return AutofillRule(
matchers,
applyInSingleOriginMode,
applyOnManualRequestOnly,
name ?: "Rule #$ruleId"
)
.also { ruleId++ }
}
}
@ -409,4 +432,5 @@ internal class AutofillStrategy private constructor(private val rules: List<Auto
}
}
internal fun strategy(block: AutofillStrategy.Builder.() -> Unit) = AutofillStrategy.Builder().apply(block).build()
internal fun strategy(block: AutofillStrategy.Builder.() -> Unit) =
AutofillStrategy.Builder().apply(block).build()

View file

@ -63,7 +63,10 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH =
"com.chrome.canary" to arrayOf("IBnfofsj779wxbzRRDxb6rBPPy/0Nm6aweNFdjmiTPw="),
"com.chrome.dev" to arrayOf("kETuX+5LvF4h3URmVDHE6x8fcaMnFqC8knvLs5Izyr8="),
"com.duckduckgo.mobile.android" to
arrayOf("u3uzHFc8RqHaf8XFKKas9DIQhFb+7FCBDH8zaU6z0tQ=", "8HB9AhwL8+b43MEbo/VwBCXVl9yjAaMeIQVWk067Gwo="),
arrayOf(
"u3uzHFc8RqHaf8XFKKas9DIQhFb+7FCBDH8zaU6z0tQ=",
"8HB9AhwL8+b43MEbo/VwBCXVl9yjAaMeIQVWk067Gwo="
),
"com.microsoft.emmx" to arrayOf("AeGZlxCoLCdJtNUMRF3IXWcLYTYInQp2anOCfIKh6sk="),
"com.opera.mini.native" to arrayOf("V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I="),
"com.opera.mini.native.beta" to arrayOf("V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I="),
@ -80,7 +83,8 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH =
"org.mozilla.klar" to arrayOf("YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w="),
"org.torproject.torbrowser" to arrayOf("IAYfBF5zfGc3XBd5TP7bQ2oDzsa6y3y5+WZCIFyizsg="),
"org.ungoogled.chromium.stable" to arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="),
"org.ungoogled.chromium.extensions.stable" to arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="),
"org.ungoogled.chromium.extensions.stable" to
arrayOf("29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk="),
"com.kiwibrowser.browser" to arrayOf("wGnqlmMy6R4KDDzFd+b1Cf49ndr3AVrQxcXvj9o/hig="),
)
@ -162,19 +166,30 @@ private val BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY =
private fun isNoAccessibilityServiceEnabled(context: Context): Boolean {
// See https://chromium.googlesource.com/chromium/src/+/447a31e977a65e2eb78804e4a09633699b4ede33
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES)
return Settings.Secure.getString(
context.contentResolver,
Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
)
.isNullOrEmpty()
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getBrowserSaveFlag(context: Context, appPackage: String): Int? =
BROWSER_SAVE_FLAG[appPackage]
?: BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY[appPackage]?.takeIf { isNoAccessibilityServiceEnabled(context) }
?: BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY[appPackage]?.takeIf {
isNoAccessibilityServiceEnabled(context)
}
internal data class BrowserAutofillSupportInfo(val multiOriginMethod: BrowserMultiOriginMethod, val saveFlags: Int?)
internal data class BrowserAutofillSupportInfo(
val multiOriginMethod: BrowserMultiOriginMethod,
val saveFlags: Int?
)
@RequiresApi(Build.VERSION_CODES.O)
internal fun getBrowserAutofillSupportInfoIfTrusted(context: Context, appPackage: String): BrowserAutofillSupportInfo? {
internal fun getBrowserAutofillSupportInfoIfTrusted(
context: Context,
appPackage: String
): BrowserAutofillSupportInfo? {
if (!isTrustedBrowser(context, appPackage)) return null
return BrowserAutofillSupportInfo(
multiOriginMethod = getBrowserMultiOriginMethod(appPackage),
@ -197,14 +212,18 @@ public enum class BrowserAutofillSupportLevel {
}
@RequiresApi(Build.VERSION_CODES.O)
private fun getBrowserAutofillSupportLevel(context: Context, appPackage: String): BrowserAutofillSupportLevel {
private fun getBrowserAutofillSupportLevel(
context: Context,
appPackage: String
): BrowserAutofillSupportLevel {
val browserInfo = getBrowserAutofillSupportInfoIfTrusted(context, appPackage)
return when {
browserInfo == null -> BrowserAutofillSupportLevel.None
appPackage in FLAKY_BROWSERS -> BrowserAutofillSupportLevel.FlakyFill
appPackage in BROWSER_SAVE_FLAG_IF_NO_ACCESSIBILITY ->
BrowserAutofillSupportLevel.PasswordFillAndSaveIfNoAccessibility
browserInfo.multiOriginMethod == BrowserMultiOriginMethod.None -> BrowserAutofillSupportLevel.PasswordFill
browserInfo.multiOriginMethod == BrowserMultiOriginMethod.None ->
BrowserAutofillSupportLevel.PasswordFill
browserInfo.saveFlags == null -> BrowserAutofillSupportLevel.GeneralFill
else -> BrowserAutofillSupportLevel.GeneralFillAndSave
}.takeUnless { supportLevel ->
@ -212,7 +231,8 @@ private fun getBrowserAutofillSupportLevel(context: Context, appPackage: String)
// (compatibility mode is only available on Android Pie and higher). Since all known browsers
// with native Autofill support offer full save support as well, we reuse the list of those
// browsers here.
supportLevel != BrowserAutofillSupportLevel.GeneralFillAndSave && Build.VERSION.SDK_INT < Build.VERSION_CODES.P
supportLevel != BrowserAutofillSupportLevel.GeneralFillAndSave &&
Build.VERSION.SDK_INT < Build.VERSION_CODES.P
}
?: BrowserAutofillSupportLevel.None
}
@ -222,9 +242,15 @@ public fun getInstalledBrowsersWithAutofillSupportLevel(
context: Context
): List<Pair<String, BrowserAutofillSupportLevel>> {
val testWebIntent = Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("http://example.org") }
val installedBrowsers = context.packageManager.queryIntentActivities(testWebIntent, PackageManager.MATCH_ALL)
val installedBrowsers =
context.packageManager.queryIntentActivities(testWebIntent, PackageManager.MATCH_ALL)
return installedBrowsers
.map { it to getBrowserAutofillSupportLevel(context, it.activityInfo.packageName) }
.filter { it.first.isDefault || it.second != BrowserAutofillSupportLevel.None }
.map { context.packageManager.getApplicationLabel(it.first.activityInfo.applicationInfo).toString() to it.second }
.map {
context
.packageManager
.getApplicationLabel(it.first.activityInfo.applicationInfo)
.toString() to it.second
}
}

View file

@ -108,9 +108,14 @@ internal class FormField(
"text",
)
private val HTML_INPUT_FIELD_TYPES_FILLABLE =
(HTML_INPUT_FIELD_TYPES_USERNAME + HTML_INPUT_FIELD_TYPES_PASSWORD + HTML_INPUT_FIELD_TYPES_OTP).toSet().toList()
(HTML_INPUT_FIELD_TYPES_USERNAME +
HTML_INPUT_FIELD_TYPES_PASSWORD +
HTML_INPUT_FIELD_TYPES_OTP)
.toSet()
.toList()
@RequiresApi(Build.VERSION_CODES.O) private fun isSupportedHint(hint: String) = hint in HINTS_FILLABLE
@RequiresApi(Build.VERSION_CODES.O)
private fun isSupportedHint(hint: String) = hint in HINTS_FILLABLE
private val EXCLUDED_TERMS =
listOf(
"url_bar", // Chrome/Edge/Firefox address bar
@ -214,7 +219,8 @@ internal class FormField(
private val hasAutocompleteHintUsername = htmlAutocomplete == "username"
val hasAutocompleteHintCurrentPassword = htmlAutocomplete == "current-password"
private val hasAutocompleteHintNewPassword = htmlAutocomplete == "new-password"
private val hasAutocompleteHintPassword = hasAutocompleteHintCurrentPassword || hasAutocompleteHintNewPassword
private val hasAutocompleteHintPassword =
hasAutocompleteHintCurrentPassword || hasAutocompleteHintNewPassword
private val hasAutocompleteHintOtp = htmlAutocomplete == "one-time-code"
// Results of hint-based field type detection
@ -238,7 +244,9 @@ internal class FormField(
// fields to the fill rules and only exclude those fields that have incompatible autocomplete
// hint.
val couldBeTwoStepHiddenPassword =
!isVisible && isHtmlPasswordField && (hasAutocompleteHintCurrentPassword || htmlAutocomplete == null)
!isVisible &&
isHtmlPasswordField &&
(hasAutocompleteHintCurrentPassword || htmlAutocomplete == null)
// Since many site put autocomplete=off on login forms for compliance reasons or since they are
// worried of the user's browser automatically (i.e., without any user interaction) filling
@ -247,7 +255,8 @@ internal class FormField(
private val excludedByHints = excludedByAutofillHints
// Only offer to fill into custom views if they explicitly opted into Autofill.
val relevantField = hasAutofillTypeText && (isTextField || autofillHints.isNotEmpty()) && !excludedByHints
val relevantField =
hasAutofillTypeText && (isTextField || autofillHints.isNotEmpty()) && !excludedByHints
// Exclude fields based on hint, resource ID or HTML name.
// Note: We still report excluded fields as relevant since they count for adjacency heuristics,
@ -260,7 +269,8 @@ internal class FormField(
notExcluded && (isAndroidPasswordField || isHtmlPasswordField || hasHintPassword)
private val isCertainPasswordField = isPossiblePasswordField && hasHintPassword
private val isLikelyPasswordField =
isPossiblePasswordField && (isCertainPasswordField || PASSWORD_HEURISTIC_TERMS.anyMatchesFieldInfo)
isPossiblePasswordField &&
(isCertainPasswordField || PASSWORD_HEURISTIC_TERMS.anyMatchesFieldInfo)
val passwordCertainty =
if (isCertainPasswordField) CertaintyLevel.Certain
else if (isLikelyPasswordField) CertaintyLevel.Likely
@ -273,17 +283,20 @@ internal class FormField(
isPossibleOtpField &&
(isCertainOtpField ||
OTP_HEURISTIC_TERMS.anyMatchesFieldInfo ||
((htmlMaxLength == null || htmlMaxLength in 6..8) && OTP_WEAK_HEURISTIC_TERMS.anyMatchesFieldInfo))
((htmlMaxLength == null || htmlMaxLength in 6..8) &&
OTP_WEAK_HEURISTIC_TERMS.anyMatchesFieldInfo))
val otpCertainty =
if (isCertainOtpField) CertaintyLevel.Certain
else if (isLikelyOtpField) CertaintyLevel.Likely
else if (isPossibleOtpField) CertaintyLevel.Possible else CertaintyLevel.Impossible
// Username field heuristics (based only on the current field)
private val isPossibleUsernameField = notExcluded && !isPossiblePasswordField && !isCertainOtpField
private val isPossibleUsernameField =
notExcluded && !isPossiblePasswordField && !isCertainOtpField
private val isCertainUsernameField = isPossibleUsernameField && hasHintUsername
private val isLikelyUsernameField =
isPossibleUsernameField && (isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo))
isPossibleUsernameField &&
(isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo))
val usernameCertainty =
if (isCertainUsernameField) CertaintyLevel.Certain
else if (isLikelyUsernameField) CertaintyLevel.Likely

View file

@ -34,12 +34,18 @@ public fun cachePublicSuffixList(context: Context) {
* Note: Invalid domains, such as IP addresses, are returned unchanged and thus never collide with
* the return value for valid domains.
*/
internal fun getPublicSuffixPlusOne(context: Context, domain: String, customSuffixes: Sequence<String>) = runBlocking {
internal fun getPublicSuffixPlusOne(
context: Context,
domain: String,
customSuffixes: Sequence<String>
) = runBlocking {
// We only feed valid domain names which are not IP addresses into getPublicSuffixPlusOne.
// We do not check whether the domain actually exists (actually, not even whether its TLD
// exists). As long as we restrict ourselves to syntactically valid domain names,
// getPublicSuffixPlusOne will return non-colliding results.
if (!Patterns.DOMAIN_NAME.matcher(domain).matches() || Patterns.IP_ADDRESS.matcher(domain).matches()) {
if (!Patterns.DOMAIN_NAME.matcher(domain).matches() ||
Patterns.IP_ADDRESS.matcher(domain).matches()
) {
domain
} else {
getCanonicalSuffix(context, domain, customSuffixes)
@ -60,7 +66,11 @@ private fun getSuffixPlusUpToOne(domain: String, suffix: String): String? {
return "$lastPrefixPart.$suffix"
}
private suspend fun getCanonicalSuffix(context: Context, domain: String, customSuffixes: Sequence<String>): String {
private suspend fun getCanonicalSuffix(
context: Context,
domain: String,
customSuffixes: Sequence<String>
): String {
val publicSuffixList = PublicSuffixListCache.getOrCachePublicSuffixList(context)
val publicSuffixPlusOne = publicSuffixList.getPublicSuffixPlusOne(domain).await() ?: return domain
var longestSuffix = publicSuffixPlusOne

View file

@ -58,7 +58,8 @@ internal class PublicSuffixList(
fun getPublicSuffixPlusOne(domain: String): Deferred<String?> =
scope.async {
when (val offset = data.getPublicSuffixOffset(domain)) {
is PublicSuffixOffset.Offset -> domain.split('.').drop(offset.value).joinToString(separator = ".")
is PublicSuffixOffset.Offset ->
domain.split('.').drop(offset.value).joinToString(separator = ".")
else -> null
}
}

View file

@ -12,7 +12,10 @@ import java.net.IDN
import mozilla.components.lib.publicsuffixlist.ext.binarySearch
/** Class wrapping the public suffix list data and offering methods for accessing rules in it. */
internal class PublicSuffixListData(private val rules: ByteArray, private val exceptions: ByteArray) {
internal class PublicSuffixListData(
private val rules: ByteArray,
private val exceptions: ByteArray
) {
private fun binarySearchRules(labels: List<ByteArray>, labelIndex: Int): String? {
return rules.binarySearch(labels, labelIndex)

View file

@ -30,7 +30,12 @@ internal object PublicSuffixListLoader {
@Suppress("MagicNumber")
private fun BufferedInputStream.readInt(): Int {
return (read() and 0xff shl 24 or (read() and 0xff shl 16) or (read() and 0xff shl 8) or (read() and 0xff))
return (read() and
0xff shl
24 or
(read() and 0xff shl 16) or
(read() and 0xff shl 8) or
(read() and 0xff))
}
private fun BufferedInputStream.readFully(size: Int): ByteArray {

View file

@ -1,11 +0,0 @@
/**
* IDEs don't support this very well for buildSrc, so we use the regular dependency format
* until that changes.
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
*/

View file

@ -115,11 +115,13 @@ constructor(
.lineSequence()
.filter { line ->
return@filter when {
USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } && !foundUsername -> {
USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } &&
!foundUsername -> {
foundUsername = true
false
}
line.startsWith("otpauth://", ignoreCase = true) || line.startsWith("totp:", ignoreCase = true) -> {
line.startsWith("otpauth://", ignoreCase = true) ||
line.startsWith("totp:", ignoreCase = true) -> {
false
}
else -> {
@ -174,7 +176,8 @@ constructor(
private fun findUsername(): String? {
extraContentString.splitToSequence("\n").forEach { line ->
for (prefix in USERNAME_FIELDS) {
if (line.startsWith(prefix, ignoreCase = true)) return line.substring(prefix.length).trimStart()
if (line.startsWith(prefix, ignoreCase = true))
return line.substring(prefix.length).trimStart()
}
}
return null

View file

@ -23,7 +23,8 @@ internal object Otp {
check(STEAM_ALPHABET.size == 26)
}
fun calculateCode(secret: String, counter: Long, algorithm: String, digits: String) = runCatching {
fun calculateCode(secret: String, counter: Long, algorithm: String, digits: String) =
runCatching {
val algo = "Hmac${algorithm.uppercase(Locale.ROOT)}"
val decodedSecret = BASE_32.decode(secret)
val secretKey = SecretKeySpec(decodedSecret, algo)

View file

@ -19,7 +19,8 @@ import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
internal class PasswordEntryTest {
private fun makeEntry(content: String) = PasswordEntry(fakeClock, testFinder, testScope, content.encodeToByteArray())
private fun makeEntry(content: String) =
PasswordEntry(fakeClock, testFinder, testScope, content.encodeToByteArray())
@Test
fun testGetPassword() {
@ -49,7 +50,10 @@ internal class PasswordEntryTest {
assertEquals("blubb", makeEntry("\nblubb").extraContentString)
assertEquals("blubb", makeEntry("blubb\npassword: foo").extraContentString)
assertEquals("blubb", makeEntry("password: foo\nblubb").extraContentString)
assertEquals("blubb\nusername: bar", makeEntry("blubb\npassword: foo\nusername: bar").extraContentString)
assertEquals(
"blubb\nusername: bar",
makeEntry("blubb\npassword: foo\nusername: bar").extraContentString
)
assertEquals("", makeEntry("\n").extraContentString)
assertEquals("", makeEntry("").extraContentString)
}

View file

@ -15,16 +15,34 @@ internal class OtpTest {
@Test
fun testOtpGeneration6Digits() {
assertEquals("953550", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333298159 / (1000 * 30), "SHA1", "6").get())
assertEquals("275379", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333571918 / (1000 * 30), "SHA1", "6").get())
assertEquals("867507", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333600517 / (1000 * 57), "SHA1", "6").get())
assertEquals(
"953550",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333298159 / (1000 * 30), "SHA1", "6").get()
)
assertEquals(
"275379",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333571918 / (1000 * 30), "SHA1", "6").get()
)
assertEquals(
"867507",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333600517 / (1000 * 57), "SHA1", "6").get()
)
}
@Test
fun testOtpGeneration10Digits() {
assertEquals("0740900914", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333655044 / (1000 * 30), "SHA1", "10").get())
assertEquals("0070632029", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333691405 / (1000 * 30), "SHA1", "10").get())
assertEquals("1017265882", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333728893 / (1000 * 83), "SHA1", "10").get())
assertEquals(
"0740900914",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333655044 / (1000 * 30), "SHA1", "10").get()
)
assertEquals(
"0070632029",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333691405 / (1000 * 30), "SHA1", "10").get()
)
assertEquals(
"1017265882",
Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333728893 / (1000 * 83), "SHA1", "10").get()
)
}
@Test
@ -42,7 +60,10 @@ internal class OtpTest {
"127764",
Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAAAAA", 1593367111963 / (1000 * 30), "SHA1", "6").get()
)
assertEquals("047515", Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAA", 1593367171420 / (1000 * 30), "SHA1", "6").get())
assertEquals(
"047515",
Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAA", 1593367171420 / (1000 * 30), "SHA1", "6").get()
)
}
@Test

Some files were not shown because too many files have changed in this diff Show more