From ccc8843cb09a8673f3d126dd61a177231ef470e6 Mon Sep 17 00:00:00 2001 From: Aditya Bhardwaj Date: Wed, 16 Sep 2026 08:05:18 +0530 Subject: [PATCH 1/9] Localize onboarding and fix the crash that translating it would cause Onboarding titles/descriptions, Skip/Next and the splash tagline rendered in English in all 16 locales. Root cause: 14 strings were marked translatable="false" in values/strings.xml, which both excluded them from every values-*/ folder and suppressed the MissingTranslation lint check that would have caught it. The Kotlin was already using stringResource() correctly. Translating the headings alone would have crashed the app on first launch: TextHighlighter located the emphasised word with fullText.indexOf("Secure") and passed the result straight to AnnotatedString.addStyle. For any translated heading indexOf returns -1, and addStyle(start = -1, ...) throws. The highlight is now marked up as [[word]] inside each string resource, so translators choose the emphasised word in their own language, and a pure-Kotlin parser strips the markers. Unbalanced or absent markers degrade to unstyled text instead of throwing. Headings auto-size 36-64sp so longer languages do not clip. Also in this release: - Arabic was offered by the in-app picker but missing from locales_config.xml, so selecting it silently did nothing. locales_config, values-*/ and the picker now agree on all 17 locales. - Saving a preview without renaming it matched itself in the duplicate-heading check, reported "heading exists" and discarded the edit. - DetailViewModel trimmed on create but not on edit, so trailing whitespace crept into stored passwords. - The bottom navigation announced the raw enum name ("BANKS") in every locale, and the alpha-0 FAB spacer was reachable by TalkBack. - The details screen's empty state read "No Previews Found". - PasswordGenerator used kotlin.random.Random rather than SecureRandom, and did not guarantee a character from each selected class. - Removed 10 string resources that were unused in code (170 entries across all locales), so they are not translated needlessly. - proguard-rules.pro kept net.sqlcipher.**, but the shipped artifact is net.zetetic:sqlcipher-android. Those rules matched nothing. The library supplies its own consumer rules; verified against mapping.txt that all 59 net.zetetic classes are kept unrenamed without any app-side rule, so the dead rules were removed rather than replaced. MissingTranslation, ExtraTranslation, ImpliedQuantity and the string-format checks are now build-breaking errors, which is what stops this recurring. Verified on an API 36 emulator: onboarding launches without crashing in hi, ar, de, ta, ja and en, with markers stripped and text localized. 16 unit tests pass, lint passes, release build is green and the Room schema is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- app/build.gradle.kts | 17 ++- app/proguard-rules.pro | 12 +- .../domain/viewModels/DetailViewModel.kt | 6 +- .../domain/viewModels/PreviewViewModel.kt | 7 +- .../screens/detail_screen/DetailPage.kt | 2 +- .../onboarding_screens/OnBoardingPage.kt | 14 +-- .../components/HighlightMarkup.kt | 56 +++++++++ .../components/OnBoardingItem.kt | 5 +- .../components/TextHighlighter.kt | 64 ++++++----- .../screens/preview_screen/PreviewPage.kt | 11 +- .../components/PreviewBottomNavigation.kt | 31 +++-- .../com/bhardwaj/passkey/utils/Categories.kt | 19 +++- .../passkey/utils/PasswordGenerator.kt | 68 ++++++++--- app/src/main/res/values-ar/strings.xml | 30 +++-- app/src/main/res/values-bn/strings.xml | 30 +++-- app/src/main/res/values-de/strings.xml | 30 +++-- app/src/main/res/values-es/strings.xml | 30 +++-- app/src/main/res/values-fr/strings.xml | 30 +++-- app/src/main/res/values-gu/strings.xml | 30 +++-- app/src/main/res/values-hi/strings.xml | 32 ++++-- app/src/main/res/values-it/strings.xml | 30 +++-- app/src/main/res/values-ja/strings.xml | 30 +++-- app/src/main/res/values-ko/strings.xml | 30 +++-- app/src/main/res/values-mr/strings.xml | 30 +++-- app/src/main/res/values-pt/strings.xml | 30 +++-- app/src/main/res/values-ru/strings.xml | 30 +++-- app/src/main/res/values-ta/strings.xml | 30 +++-- app/src/main/res/values-te/strings.xml | 30 +++-- app/src/main/res/values-zh/strings.xml | 30 +++-- app/src/main/res/values/strings.xml | 50 +++++---- app/src/main/res/xml/locales_config.xml | 1 + .../bhardwaj/passkey/HighlightMarkupTest.kt | 106 ++++++++++++++++++ .../bhardwaj/passkey/PasswordGeneratorTest.kt | 102 +++++++++++++++++ gradlew | 0 34 files changed, 770 insertions(+), 283 deletions(-) create mode 100644 app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/HighlightMarkup.kt create mode 100644 app/src/test/java/com/bhardwaj/passkey/HighlightMarkupTest.kt create mode 100644 app/src/test/java/com/bhardwaj/passkey/PasswordGeneratorTest.kt mode change 100644 => 100755 gradlew diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 33be3c1..9625743 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.bhardwaj.passkey" minSdk = 28 targetSdk = 36 - versionCode = 43 - versionName = "5.5.1" + versionCode = 44 + versionName = "5.5.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -70,6 +70,19 @@ android { compose = true buildConfig = true } + lint { + // The onboarding strings shipped in English to all 16 locales for several releases + // because they were marked translatable="false", which also suppressed this check. + // Failing the build is what stops that recurring. + error += setOf( + "MissingTranslation", + "ExtraTranslation", + "ImpliedQuantity", + "StringFormatInvalid", + "StringFormatMatches" + ) + abortOnError = true + } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 22639f5..3ff03e3 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -20,5 +20,13 @@ # hide the original source file name. #-renamesourcefileattribute SourceFile --keep class net.sqlcipher.** { *; } --keep class net.sqlcipher.database.* { *; } \ No newline at end of file +# SQLCipher. +# The shipped artifact is net.zetetic:sqlcipher-android, whose classes live in +# net.zetetic.database.sqlcipher.**. This module's own AAR supplies consumer ProGuard rules +# (keeping native methods, constructors and mNativeHandle), so no app-side keep rules are +# needed here - verified against app/build/outputs/mapping/release/mapping.txt, where all 61 +# net.zetetic classes are kept unrenamed. +# +# The previous rules in this file referenced the legacy `net.sqlcipher.**` package, which this +# app has not used since migrating to sqlcipher-android. They matched nothing and were removed +# so they cannot be mistaken for load-bearing configuration. diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt index 34aa1eb..e95cfad 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt @@ -161,8 +161,10 @@ class DetailViewModel @Inject constructor( repository.upsertDetails( it.copy( previewId = previewId, - question = detailTitle.value, - answer = detailResponse.value, + // Trimmed to match the create path above. A trailing space in a + // stored password fails silently wherever it is pasted. + question = newDetail.question, + answer = newDetail.answer, ) ) } ?: repository.upsertDetails(newDetail) diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt index d42b218..8265817 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt @@ -132,7 +132,12 @@ class PreviewViewModel @Inject constructor( newPreview.heading, newPreview.categoryName.toString() ) - if (existingPreview != null) { + // When editing, the lookup finds the very row being edited. Treating that as + // a clash meant saving an edit without renaming it reported "heading exists" + // and silently discarded the edit. + val isClashWithAnotherRow = + existingPreview != null && existingPreview.previewId != preview?.previewId + if (isClashWithAnotherRow) { preview = null savedStateHandle[PREVIEW_HEADING] = "" isSheetOpen = false diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt index 46f34fb..9e3197a 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt @@ -159,7 +159,7 @@ fun DetailScreen( .fillMaxWidth() .padding(16.dp), painter = painterResource(id = R.drawable.icon_empty_list), - contentDescription = "No Previews Found" + contentDescription = stringResource(id = R.string.no_details_found) ) } else { LazyColumn( diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/OnBoardingPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/OnBoardingPage.kt index 21cb9ba..e5b540b 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/OnBoardingPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/OnBoardingPage.kt @@ -26,8 +26,7 @@ import kotlinx.coroutines.launch data class OnBoardingScreen( val title: String, - val description: String, - val highlightedText: List + val description: String ) @Composable @@ -35,21 +34,20 @@ fun OnBoardingScreen( onNavigate: (UiEvents.Navigate) -> Unit, viewModel: OnBoardingViewModel = hiltViewModel() ) { + // The emphasised word is marked with [[ ]] inside each heading string resource, so each + // translation controls which of its own words is highlighted. val pages = listOf( OnBoardingScreen( title = stringResource(id = R.string.first_heading), - description = stringResource(id = R.string.first_description), - highlightedText = listOf("Secure") + description = stringResource(id = R.string.first_description) ), OnBoardingScreen( title = stringResource(id = R.string.second_heading), - description = stringResource(id = R.string.second_description), - highlightedText = listOf("Passwords") + description = stringResource(id = R.string.second_description) ), OnBoardingScreen( title = stringResource(id = R.string.third_heading), - description = stringResource(id = R.string.third_description), - highlightedText = listOf("Autofill") + description = stringResource(id = R.string.third_description) ) ) diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/HighlightMarkup.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/HighlightMarkup.kt new file mode 100644 index 0000000..1e9b249 --- /dev/null +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/HighlightMarkup.kt @@ -0,0 +1,56 @@ +package com.bhardwaj.passkey.presentation.screens.onboarding_screens.components + +/** + * A span of [text] that should be rendered with the highlight colour. + */ +data class HighlightSpan(val start: Int, val endExclusive: Int) + +/** + * The result of stripping highlight markers out of a raw string resource. + * + * @param text the display text, with all markers removed + * @param spans the ranges of [text] that were wrapped in markers + */ +data class MarkedUpText(val text: String, val spans: List) + +private const val OPEN = "[[" +private const val CLOSE = "]]" + +/** + * Parses `[[ ]]` highlight markers out of a translatable string. + * + * The marker lives inside the string resource so that translators choose which word is + * emphasised in their own language. Previously the highlighted word was a hardcoded English + * literal matched with `indexOf`, which returned -1 for every translated heading and crashed + * `AnnotatedString.addStyle`. + * + * This parser never throws. Unbalanced, empty or absent markers simply yield fewer spans, so a + * translator dropping a marker degrades to unstyled text rather than a crash. + */ +fun parseHighlightMarkup(raw: String): MarkedUpText { + val out = StringBuilder(raw.length) + val spans = mutableListOf() + var i = 0 + var openAt: Int? = null + + while (i < raw.length) { + when { + openAt == null && raw.startsWith(OPEN, i) -> { + openAt = out.length + i += OPEN.length + } + + openAt != null && raw.startsWith(CLOSE, i) -> { + if (openAt < out.length) spans += HighlightSpan(openAt, out.length) + openAt = null + i += CLOSE.length + } + + else -> { + out.append(raw[i]) + i++ + } + } + } + return MarkedUpText(text = out.toString(), spans = spans) +} diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/OnBoardingItem.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/OnBoardingItem.kt index 4ca4743..7d184ff 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/OnBoardingItem.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/OnBoardingItem.kt @@ -33,12 +33,11 @@ fun OnBoardingItem( ) { Image( painter = painterResource(id = R.drawable.icon_logo), - contentDescription = "App Logo" + contentDescription = null ) TextHighlighter( modifier = Modifier.padding(top = 72.dp), - fullText = screen.title, - highlightedText = screen.highlightedText + markedUpText = screen.title ) Text( modifier = Modifier.padding(top = 24.dp), diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/TextHighlighter.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/TextHighlighter.kt index 2034c0c..2e00c41 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/TextHighlighter.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/onboarding_screens/components/TextHighlighter.kt @@ -1,50 +1,62 @@ package com.bhardwaj.passkey.presentation.screens.onboarding_screens.components +import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.bhardwaj.passkey.presentation.theme.BebasNeue +/** + * Renders an onboarding heading, colouring the words the translator wrapped in `[[ ]]`. + * + * The heading auto-sizes between 36sp and 64sp: the strings are laid out on three lines and + * translations of "Generate\nSecure\nPasswords." are considerably longer in several supported + * languages, which would clip at a fixed 64sp on small screens. + */ @Composable fun TextHighlighter( + markedUpText: String, modifier: Modifier = Modifier, - fullText: String, - highlightedText: List, ) { - val annotatedString = buildAnnotatedString { - append(fullText) - highlightedText.forEach { text -> - val startIndex = fullText.indexOf(text) - val endIndex = startIndex + text.length - addStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary - ), - start = startIndex, - end = endIndex - ) + val highlightColor = MaterialTheme.colorScheme.primary + val parsed = remember(markedUpText) { parseHighlightMarkup(markedUpText) } + + val annotatedString = remember(parsed, highlightColor) { + buildAnnotatedString { + append(parsed.text) + parsed.spans.forEach { span -> + // Defensive: the parser cannot currently emit out-of-bounds spans, but clamping + // here means a future change can never reintroduce the addStyle crash. + val start = span.start.coerceIn(0, parsed.text.length) + val end = span.endExclusive.coerceIn(start, parsed.text.length) + if (end > start) { + addStyle( + style = SpanStyle(color = highlightColor), + start = start, + end = end + ) + } + } } - addStyle( - style = SpanStyle( - fontFamily = BebasNeue, - fontSize = 64.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - ), - start = 0, - end = fullText.length - ) } + Text( modifier = modifier, text = annotatedString, - lineHeight = 70.sp, + autoSize = TextAutoSize.StepBased(minFontSize = 36.sp, maxFontSize = 64.sp), + fontFamily = BebasNeue, + fontWeight = FontWeight.Normal, + fontStyle = FontStyle.Normal, + // Relative to the resolved font size, so it tracks auto-sizing. + lineHeight = 1.1.em, color = MaterialTheme.colorScheme.secondary ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt index b99c861..5e48793 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt @@ -78,12 +78,9 @@ fun PreviewScreen( viewModel: PreviewViewModel = hiltViewModel() ) { val categoryName by viewModel.categoryName.collectAsState() - val categoryNameMap = mapOf( - Categories.BANKS.name to stringResource(id = R.string.banks), - Categories.APPS.name to stringResource(id = R.string.apps), - Categories.MAILS.name to stringResource(id = R.string.mails), - Categories.OTHERS.name to stringResource(id = R.string.others), - ) + // Localized display names come from Categories.labelRes, which the bottom navigation + // also uses, so the two can no longer drift apart. + val categoryNameMap = Categories.entries.associate { it.name to stringResource(it.labelRes) } val previewHeading by viewModel.previewHeading.collectAsState() val bottomSheetHeading by viewModel.bottomSheetHeading.collectAsState() @@ -212,7 +209,7 @@ fun PreviewScreen( .fillMaxWidth() .padding(16.dp), painter = painterResource(id = R.drawable.icon_empty_list), - contentDescription = "No Previews Found" + contentDescription = stringResource(id = R.string.no_previews_found) ) } else { LazyColumn( diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomNavigation.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomNavigation.kt index b2c39ca..cc90236 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomNavigation.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomNavigation.kt @@ -24,36 +24,42 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.dp import com.bhardwaj.passkey.utils.Categories data class BottomNavigationItem( - val title: String, + val category: Categories, val icon: ImageVector, val isVisible: Boolean = true -) +) { + /** The persisted enum name, used as the navigation/selection key. */ + val title: String get() = category.name +} val bottomNavigationList = listOf( BottomNavigationItem( - title = Categories.BANKS.name, + category = Categories.BANKS, icon = Icons.Filled.AccountBalance ), BottomNavigationItem( - title = Categories.APPS.name, + category = Categories.APPS, icon = Icons.Filled.Gamepad ), + // Invisible spacer that reserves room for the centre FAB notch. It is not a destination. BottomNavigationItem( - title = Categories.BANKS.name, + category = Categories.BANKS, icon = Icons.Filled.AccountBalance, isVisible = false, ), BottomNavigationItem( - title = Categories.MAILS.name, + category = Categories.MAILS, icon = Icons.Filled.Email ), BottomNavigationItem( - title = Categories.OTHERS.name, + category = Categories.OTHERS, icon = Icons.AutoMirrored.Filled.Article ) ) @@ -76,8 +82,17 @@ fun MainBottomNavigation( bottomNavigationList.forEachIndexed { index, item -> Icon( imageVector = item.icon, - contentDescription = item.title, + // Previously the raw enum name ("BANKS"), announced in English in every locale. + contentDescription = if (item.isVisible) { + stringResource(id = item.category.labelRes) + } else { + null + }, modifier = Modifier + .then( + // The alpha-0 spacer stays reachable by TalkBack without this. + if (item.isVisible) Modifier else Modifier.clearAndSetSemantics { } + ) .size(42.dp) .padding(8.dp) .clickable( diff --git a/app/src/main/java/com/bhardwaj/passkey/utils/Categories.kt b/app/src/main/java/com/bhardwaj/passkey/utils/Categories.kt index 49989b1..bae470a 100644 --- a/app/src/main/java/com/bhardwaj/passkey/utils/Categories.kt +++ b/app/src/main/java/com/bhardwaj/passkey/utils/Categories.kt @@ -1,8 +1,15 @@ package com.bhardwaj.passkey.utils -enum class Categories { - BANKS, - MAILS, - APPS, - OTHERS -} \ No newline at end of file +import androidx.annotation.StringRes +import com.bhardwaj.passkey.R + +/** + * The entry categories. The enum *name* is the persisted value (Room column and backup files), + * so it must not change; [labelRes] carries the localized display name instead. + */ +enum class Categories(@param:StringRes val labelRes: Int) { + BANKS(R.string.banks), + MAILS(R.string.mails), + APPS(R.string.apps), + OTHERS(R.string.others) +} diff --git a/app/src/main/java/com/bhardwaj/passkey/utils/PasswordGenerator.kt b/app/src/main/java/com/bhardwaj/passkey/utils/PasswordGenerator.kt index 2f2a199..6c001af 100644 --- a/app/src/main/java/com/bhardwaj/passkey/utils/PasswordGenerator.kt +++ b/app/src/main/java/com/bhardwaj/passkey/utils/PasswordGenerator.kt @@ -1,28 +1,60 @@ package com.bhardwaj.passkey.utils +import java.security.SecureRandom +import java.util.Collections +import java.util.Random + object PasswordGenerator { + + private const val UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + private const val LOWER = "abcdefghijklmnopqrstuvwxyz" + private const val DIGITS = "0123456789" + private const val SPECIAL = "!@#$%^&*()_+-=[]{}|;:,.<>?" + + /** + * Deliberately [SecureRandom] and not `SecureRandom.getInstanceStrong()`, which can block on + * `/dev/random`. `setSeed` is never called: on API 26+ the platform provider is already seeded + * from the kernel, and seeding it ourselves would only reduce entropy. + */ + private val secureRandom: Random = SecureRandom() + + /** + * Generates a password of [length] characters. + * + * Guarantees at least one character from every selected class, which the previous + * implementation did not — a 16-character "password with symbols" could legitimately contain + * no symbol at all. If no class is selected it falls back to lowercase. + * + * @param random injectable purely so tests can seed it deterministically; production always + * uses the [SecureRandom] default. + */ fun generate( length: Int, includeUpper: Boolean, includeLower: Boolean, includeNumbers: Boolean, - includeSpecial: Boolean + includeSpecial: Boolean, + random: Random = secureRandom ): String { - val upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - val lower = "abcdefghijklmnopqrstuvwxyz" - val numbers = "0123456789" - val special = "!@#$%^&*()_+-=[]{}|;:,.<>?" - - var charPool = "" - if (includeUpper) charPool += upper - if (includeLower) charPool += lower - if (includeNumbers) charPool += numbers - if (includeSpecial) charPool += special - - if (charPool.isEmpty()) charPool = lower - - return (1..length) - .map { charPool.random() } - .joinToString("") + val pools = buildList { + if (includeUpper) add(UPPER) + if (includeLower) add(LOWER) + if (includeNumbers) add(DIGITS) + if (includeSpecial) add(SPECIAL) + }.ifEmpty { listOf(LOWER) } + + // Cannot satisfy "one per class" in fewer characters than there are classes. + val size = maxOf(length, pools.size) + val union = pools.joinToString(separator = "") + + val characters = ArrayList(size) + // nextInt(bound) performs rejection sampling internally, so it is unbiased; a manual + // modulo would reintroduce bias. + pools.forEach { pool -> characters.add(pool[random.nextInt(pool.length)]) } + repeat(size - pools.size) { characters.add(union[random.nextInt(union.length)]) } + + // Collections.shuffle(list, random) — NOT Kotlin's shuffled(), which uses kotlin.random. + Collections.shuffle(characters, random) + return characters.joinToString(separator = "") } -} \ No newline at end of file +} diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index fbb9b9f..4ed2f7c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1,5 +1,23 @@ - + + لا توجد إدخالات بعد + لا توجد تفاصيل بعد + + إصدار التطبيق %1$s + مدير كلمات المرور الوحيد الذي ستحتاج إليه + + + + تخطٍّ + التالي + أنشئ\nكلمات مرور\n[[آمنة]]. + كل\n[[كلمات مرورك]]\nهنا. + لا تكتبها،\n[[املأ]] بياناتك\nتلقائيًا. + توقّف عن استخدام كلمات مرور ضعيفة لحساباتك على الإنترنت، وارتقِ بها مع PassKey. احصل على كلمات مرور آمنة يصعب اختراقها. + احفظ كل كلمات مرورك وأدرها من مكان واحد. لا تحفظ مئات كلمات المرور، بل واحدة فقط. + لا تُعرّض كلمات مرورك للخطر بكتابتها أمام الآخرين، دع PassKey يملأها تلقائيًا ويحافظ على أمان بياناتك. + البنوك البريد @@ -20,7 +38,6 @@ تصدير البيانات - اكتب هنا… اكتب عنوانك هنا… اكتب ردك هنا… اكتب عنوانك هنا… @@ -35,12 +52,7 @@ مرحبًا بك في مدير PassKey - فشل التوثيق. حدث خطأ ما.. - يرجى تعيين كلمة مرور لجهازك أولاً. - الوصول إلى التخزين مطلوب لتصدير قاعدة البيانات. - تم رفض الإذن - حسنًا تغيير @@ -62,15 +74,11 @@ حالة الأمان تحليل الأمان - درجة الأمان كلمات مرور ضعيفة كلمات مرور مكررة - كلمات مرور آمنة ممتاز يحتاج إلى انتباه حرج - تم العثور على %1$d كلمات مرور ضعيفة - تم العثور على %1$d كلمات مرور مكررة مستخدمة %1$d مرات لم يتم العثور على مشاكل. أحسنت! \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 56fb2e3..6a115fb 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -1,5 +1,23 @@ - + + এখনও কোনো এন্ট্রি নেই + এখনও কোনো বিবরণ নেই + + অ্যাপ সংস্করণ %1$s + আপনার প্রয়োজনীয় একমাত্র পাসওয়ার্ড ম্যানেজার + + + + এড়িয়ে যান + পরবর্তী + তৈরি করুন\n[[সুরক্ষিত]]\nপাসওয়ার্ড। + আপনার সব\n[[পাসওয়ার্ড]]\nএখানেই। + টাইপ নয়,\nতথ্য [[স্বয়ংক্রিয়ভাবে]]\nপূরণ করুন। + আপনার অনলাইন অ্যাকাউন্টে দুর্বল পাসওয়ার্ড ব্যবহার করা বন্ধ করুন, PassKey দিয়ে এক ধাপ এগিয়ে যান। সবচেয়ে সুরক্ষিত ও ভাঙা কঠিন পাসওয়ার্ড পান। + এক জায়গা থেকেই আপনার সব পাসওয়ার্ড সংরক্ষণ ও পরিচালনা করুন। শত শত পাসওয়ার্ড নয়, শুধু একটি মনে রাখুন। + প্রকাশ্যে টাইপ করে আপনার পাসওয়ার্ড ঝুঁকিতে ফেলবেন না, PassKey-কে সেগুলি স্বয়ংক্রিয়ভাবে পূরণ করতে দিন এবং আপনার তথ্য সুরক্ষিত রাখুন। + ব্যাংক মেইল @@ -20,7 +38,6 @@ তথ্য রপ্তানি - এখানে লিখুন… আপনার শিরোনাম এখানে লিখুন… আপনার প্রতিক্রিয়া এখানে লিখুন… আপনার শিরোনাম এখানে লিখুন… @@ -35,12 +52,7 @@ PassKey ম্যানেজারে আপনাকে স্বাগতম - প্রাধিকারীকরণ ব্যর্থ হয়েছে। কিছু ভুল হয়েছে.. - দয়া করে আপনার ডিভাইসের জন্য পাসওয়ার্ড সেট করুন। - ডেটাবেস রপ্তানির জন্য সংগ্রহ অ্যাক্সেস প্রয়োজন। - অনুমতি অস্বীকৃত - ঠিক আছে পরিবর্তন @@ -62,15 +74,11 @@ নিরাপত্তা স্থিতি নিরাপত্তা বিশ্লেষণ - নিরাপত্তা স্কোর দুর্বল পাসওয়ার্ড পুনঃব্যবহৃত পাসওয়ার্ড - নিরাপদ পাসওয়ার্ড চমৎকার মনোযোগ প্রয়োজন জটিল - %1$d টি দুর্বল পাসওয়ার্ড পাওয়া গেছে - %1$d টি পুনঃব্যবহৃত পাসওয়ার্ড পাওয়া গেছে %1$d বার ব্যবহৃত হয়েছে কোনো সমস্যা পাওয়া যায়নি। দারুণ! diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 90c83d1..8e413f5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,5 +1,23 @@ - + + Noch keine Einträge + Noch keine Details + + App-Version %1$s + Der einzige Passwort-Manager, den du je brauchst + + + + Überspringen + Weiter + Erstelle\n[[sichere]]\nPasswörter. + All deine\n[[Passwörter]]\nan einem Ort. + Nicht tippen,\nZugangsdaten\n[[automatisch]] füllen. + Verwende keine unsicheren Passwörter mehr für deine Online-Konten – mit PassKey geht es besser. Hol dir besonders sichere und schwer zu knackende Passwörter. + Speichere und verwalte all deine Passwörter an einem Ort. Merke dir nicht Hunderte von Passwörtern, sondern nur eines. + Gefährde deine Passwörter nicht, indem du sie in der Öffentlichkeit eintippst – lass sie PassKey automatisch ausfüllen und halte deine Zugangsdaten sicher. + Banken E-Mails @@ -20,7 +38,6 @@ Daten exportieren - Hier eingeben… Geben Sie hier Ihren Titel ein… Geben Sie hier Ihre Antwort ein… Geben Sie hier Ihre Überschrift ein… @@ -35,12 +52,7 @@ Willkommen beim PassKey Manager - Authentifizierung fehlgeschlagen. Etwas ist schiefgegangen.. - Bitte legen Sie zuerst ein Passwort für Ihr Gerät fest. - Speicherzugriff ist erforderlich, um die Datenbank zu exportieren. - Berechtigung verweigert - OK Ändern @@ -62,15 +74,11 @@ Sicherheitsstatus Sicherheitsanalyse - Sicherheitsbewertung Schwache Passwörter Wiederverwendete Passwörter - Sichere Passwörter Ausgezeichnet Aufmerksamkeit erforderlich Kritisch - %1$d schwache Passwörter gefunden - %1$d wiederverwendete Passwörter gefunden %1$d mal verwendet Keine Probleme gefunden. Gut gemacht! \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 84fc626..447b9ee 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,5 +1,23 @@ - + + Aún no hay entradas + Aún no hay detalles + + Versión de la app %1$s + El único gestor de contraseñas que necesitarás + + + + Omitir + Siguiente + Crea\ncontraseñas\n[[seguras]]. + Todas tus\n[[contraseñas]]\nestán aquí. + No escribas,\n[[autocompleta]]\ntus credenciales. + Deja de usar contraseñas inseguras en tus cuentas en línea y da el salto con PassKey. Consigue contraseñas muy seguras y difíciles de descifrar. + Guarda y gestiona todas tus contraseñas desde un solo lugar. No memorices cientos de contraseñas, solo una. + No pongas en riesgo tus contraseñas escribiéndolas en público; deja que PassKey las autocomplete y mantenga tus credenciales seguras. + Bancos Correos @@ -20,7 +38,6 @@ Exportar Datos - Escriba aquí… Escriba su título aquí… Escriba su respuesta aquí… Escriba su encabezado aquí… @@ -35,12 +52,7 @@ Bienvenido a PassKey Manager - Autenticación Fallida. Algo Salió Mal.. - Por favor establezca la contraseña de su dispositivo primero. - Se necesita acceso al almacenamiento para exportar la base de datos. - Permiso Denegado - OK Cambiar @@ -62,15 +74,11 @@ Estado de seguridad Análisis de seguridad - Puntuación de seguridad Contraseñas débiles Contraseñas reutilizadas - Contraseñas seguras Excelente Necesita atención Crítico - Se encontraron %1$d contraseñas débiles - Se encontraron %1$d contraseñas reutilizadas Usado %1$d veces No se encontraron problemas. ¡Bien hecho! diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ad6a7da..f95faa4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,5 +1,23 @@ - + + Aucune entrée pour l\'instant + Aucun détail pour l\'instant + + Version de l\'application %1$s + Le seul gestionnaire de mots de passe dont vous aurez besoin + + + + Passer + Suivant + Créez des\nmots de passe\n[[sécurisés]]. + Tous vos\n[[mots de passe]]\nsont ici. + Ne tapez plus,\n[[remplissez]]\nautomatiquement. + Arrêtez d\'utiliser des mots de passe peu sûrs pour vos comptes en ligne, passez à PassKey. Obtenez des mots de passe très sécurisés et difficiles à craquer. + Stockez et gérez tous vos mots de passe au même endroit. Ne retenez pas des centaines de mots de passe, un seul suffit. + Ne compromettez pas vos mots de passe en les saisissant en public : laissez PassKey les remplir automatiquement et garder vos identifiants en sécurité. + Banques Courriels @@ -20,7 +38,6 @@ Exporter les données - Tapez ici… Tapez votre titre ici… Tapez votre réponse ici… Tapez votre en-tête ici… @@ -35,12 +52,7 @@ Bienvenue dans PassKey Manager - Échec de l\'authentification. Quelque chose s\'est mal passé.. - Veuillez définir un mot de passe pour votre appareil d\'abord. - L\'accès au stockage est requis pour exporter la base de données. - Autorisation refusée - OK Changer @@ -62,15 +74,11 @@ État de sécurité Analyse de sécurité - Score de sécurité Mots de passe faibles Mots de passe réutilisés - Mots de passe sûrs Excellent Nécessite une attention Critique - %1$d mots de passe faibles trouvés - %1$d mots de passe réutilisés trouvés Utilisé %1$d fois Aucun problème trouvé. Bien joué ! diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index 38d8feb..5e7bedd 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -1,5 +1,23 @@ - + + હજી કોઈ એન્ટ્રી નથી + હજી કોઈ વિગત નથી + + ઍપ સંસ્કરણ %1$s + તમને જરૂરી એકમાત્ર પાસવર્ડ મેનેજર + + + + છોડો + આગળ + બનાવો\n[[સુરક્ષિત]]\nપાસવર્ડ. + તમારા બધા\n[[પાસવર્ડ]]\nઅહીં છે. + ટાઇપ નહીં,\nવિગતો\n[[ઑટોફિલ]] કરો. + તમારા ઓનલાઇન ખાતાં માટે અસુરક્ષિત પાસવર્ડ વાપરવાનું બંધ કરો, PassKey સાથે આગળ વધો. સૌથી સુરક્ષિત અને તોડવા મુશ્કેલ પાસવર્ડ મેળવો. + તમારા બધા પાસવર્ડ એક જ જગ્યાએથી સાચવો અને સંભાળો. સેંકડો પાસવર્ડ નહીં, ફક્ત એક જ યાદ રાખો. + જાહેરમાં ટાઇપ કરીને તમારા પાસવર્ડ જોખમમાં ન મૂકો, PassKey ને તે ઑટોફિલ કરવા દો અને તમારી વિગતો સુરક્ષિત રાખો. + બેંકો મેલ @@ -20,7 +38,6 @@ ડેટા નિર્યાત કરો - અહીં ટાઇપ કરો… તમારો શીર્ષક અહીં ટાઇપ કરો… તમારો જવાબ અહીં ટાઇપ કરો… તમારો શીર્ષક અહીં ટાઇપ કરો… @@ -35,12 +52,7 @@ PassKey મેનેજર માં આપનું સ્વાગત છે - પ્રમાણીકરણ નિષ્ફળ થયું. કંઈક ખોટું થયું છે.. - કૃપા કરીને પ્રથમ તમારા ડિવાઇસ માટે પાસવર્ડ સેટ કરો. - ડેટાબેઝ નિર્યાત કરવા માટે સ્ટોરેજ ઍક્સેસ જરૂરી છે. - પરવાનગી નકારી - ઠીક છે બદલવું @@ -62,15 +74,11 @@ સુરક્ષા સ્થિતિ સુરક્ષા વિશ્લેષણ - સુરક્ષા સ્કોર નબળા પાસવર્ડ્સ પુનઃઉપયોગમાં લેવાયેલા પાસવર્ડ્સ - સુરક્ષિત પાસવર્ડ્સ ઉત્તમ ધ્યાન આપવાની જરૂર છે ગંભીર - %1$d નબળા પાસવર્ડ્સ મળ્યા - %1$d પુનઃઉપયોગમાં લેવાયેલા પાસવર્ડ્સ મળ્યા %1$d વખત ઉપયોગ થયો કોઈ સમસ્યા મળી નથી. સરસ! diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 931ac0f..22e3195 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1,5 +1,23 @@ - + + अभी कोई प्रविष्टि नहीं + अभी कोई विवरण नहीं + + ऐप संस्करण %1$s + आपको बस इसी पासवर्ड मैनेजर की ज़रूरत है + + + + छोड़ें + आगे + बनाएँ\n[[सुरक्षित]]\nपासवर्ड। + आपके सारे\n[[पासवर्ड]]\nयहीं हैं। + टाइप नहीं,\nजानकारी\n[[ऑटोफ़िल]] करें। + अपने ऑनलाइन खातों के लिए असुरक्षित पासवर्ड इस्तेमाल करना बंद करें, PassKey के साथ आगे बढ़ें। सबसे सुरक्षित और मुश्किल से टूटने वाले पासवर्ड पाएँ। + अपने सभी पासवर्ड एक ही जगह से सहेजें और संभालें। सैकड़ों पासवर्ड नहीं, बस एक याद रखें। + सबके सामने टाइप करके अपने पासवर्ड जोखिम में न डालें, PassKey को उन्हें ऑटोफ़िल करने दें और अपनी जानकारी सुरक्षित रखें। + बैंक मेल @@ -20,7 +38,6 @@ फ़ाइलें निर्यात करें - यहा लिखें… यहाँ अपने शीर्षक लिखें… अपना उत्तर यहां लिखें… अपना शीर्षक यहां लिखें… @@ -35,12 +52,7 @@ PassKey प्रबंधक में आपका स्वागत है - प्रमाणीकरण विफल हो गया। कुछ गलत हो गया.. - कृपया पहले अपने डिवाइस के लिए पासवर्ड सेट करें। - संग्रहण अनुमति की आवश्यकता है - अनुमति नहीं है - स्वीकृति दें भाषा @@ -62,15 +74,11 @@ सुरक्षा स्थिति सुरक्षा विश्लेषण - सुरक्षा स्कोर कमजोर पासवर्ड पुनः उपयोग किए गए पासवर्ड - सुरक्षित पासवर्ड बहुत बढ़िया ध्यान देने की आवश्यकता है गंभीर - %1$d कमजोर पासवर्ड मिले - %1$d पुनः उपयोग किए गए पासवर्ड मिले %1$d बार उपयोग किया गया - कोई समस्या नहीं मिली। बहुत बढ़िया!v + कोई समस्या नहीं मिली। बहुत बढ़िया! \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b2af754..d80980e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1,5 +1,23 @@ - + + Nessuna voce per ora + Nessun dettaglio per ora + + Versione app %1$s + L\'unico gestore di password di cui avrai bisogno + + + + Salta + Avanti + Genera\npassword\n[[sicure]]. + Tutte le tue\n[[password]]\nsono qui. + Non digitare,\n[[compila]]\nle credenziali. + Smetti di usare password poco sicure per i tuoi account online, fai il salto con PassKey. Ottieni password sicurissime e difficili da violare. + Salva e gestisci tutte le tue password da un unico posto. Non ricordare centinaia di password, ricordane una sola. + Non mettere a rischio le tue password digitandole in pubblico: lascia che PassKey le compili automaticamente e protegga le tue credenziali. + Banche Email @@ -20,7 +38,6 @@ Esporta Dati - Digita qui… Digita il tuo titolo qui… Digita qui la tua risposta… Digita qui il tuo titolo… @@ -35,12 +52,7 @@ Benvenuti in PassKey Manager - Autenticazione Fallita. Qualcosa è andato storto.. - Si prega di impostare prima la password per il dispositivo. - È necessario l\'accesso alla memoria per esportare il database. - Permesso Negato - OK Cambia @@ -62,15 +74,11 @@ Stato di Sicurezza Analisi di Sicurezza - Punteggio di Sicurezza Password Deboli Password Riutilizzate - Password Sicure Eccellente Richiede Attenzione Critico - Trovate %1$d password deboli - Trovate %1$d password riutilizzate Usata %1$d volte Nessun problema trovato. Ottimo lavoro! diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 581c4be..668931f 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1,5 +1,23 @@ - + + まだ項目がありません + まだ詳細がありません + + アプリバージョン %1$s + 必要なパスワード管理は、これひとつ + + + + スキップ + 次へ + [[安全な]]\nパスワードを\n作成。 + すべての\n[[パスワード]]が\nここに。 + 入力せずに\n認証情報を\n[[自動入力]]。 + オンラインアカウントに安全でないパスワードを使うのはやめて、PassKey で一歩先へ。最も安全で解読されにくいパスワードを手に入れましょう。 + すべてのパスワードをひとつの場所で保存・管理。何百ものパスワードではなく、覚えるのはひとつだけ。 + 人前で入力してパスワードを危険にさらさないでください。PassKey に自動入力させて、認証情報を安全に保ちましょう。 + 銀行 メール @@ -20,7 +38,6 @@ データをエクスポート - ここに入力… ここにタイトルを入力… ここに応答を入力… ここに見出しを入力… @@ -35,12 +52,7 @@ PassKey Managerへようこそ - 認証に失敗しました。 何かがうまくいかない.. - まずデバイスのパスワードを設定してください。 - データベースをエクスポートするにはストレージアクセスが必要です。 - 許可が拒否されました - OK 変更 @@ -62,15 +74,11 @@ 安全性ステータス セキュリティ分析 - セキュリティスコア 脆弱なパスワード 使い回しのパスワード - 安全なパスワード 優秀 注意が必要 危険 - %1$d 個の脆弱なパスワードが見つかりました - %1$d 個の使い回しパスワードが見つかりました %1$d 回使用 問題は見つかりませんでした。素晴らしい! \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index ea8f025..80f19c6 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,5 +1,23 @@ - + + 아직 항목이 없습니다 + 아직 세부 정보가 없습니다 + + 앱 버전 %1$s + 당신에게 필요한 단 하나의 비밀번호 관리자 + + + + 건너뛰기 + 다음 + [[안전한]]\n비밀번호를\n만드세요. + 모든\n[[비밀번호]]가\n여기에. + 입력하지 말고\n자격 증명을\n[[자동 완성]]. + 온라인 계정에 취약한 비밀번호를 쓰지 마세요. PassKey로 한 단계 올라서세요. 가장 안전하고 뚫기 어려운 비밀번호를 만들어 드립니다. + 모든 비밀번호를 한곳에서 저장하고 관리하세요. 수백 개를 외울 필요 없이 하나만 기억하면 됩니다. + 사람들 앞에서 입력하다 비밀번호를 노출하지 마세요. PassKey가 자동으로 채워 자격 증명을 안전하게 지켜 드립니다. + 은행 메일 @@ -20,7 +38,6 @@ 데이터 내보내기 - 여기에 입력… 여기에 제목 입력… 여기에 응답 입력… 여기에 제목 입력… @@ -35,12 +52,7 @@ PassKey Manager에 오신 것을 환영합니다 - 인증 실패했습니다. 문제가 발생했습니다.. - 먼저 장치의 비밀번호를 설정하십시오. - 데이터베이스를 내보내려면 저장소 액세스가 필요합니다. - 허가가 거부되었습니다 - 확인 변경 @@ -62,15 +74,11 @@ 안전 상태 보안 분석 - 보안 점수 취약한 비밀번호 재사용된 비밀번호 - 안전한 비밀번호 훌륭함 주의 필요 심각함 - %1$d개의 취약한 비밀번호 발견 - %1$d개의 재사용된 비밀번호 발견 %1$d회 사용됨 문제가 발견되지 않았습니다. 잘하셨습니다! \ No newline at end of file diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index c84027a..657cd85 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -1,5 +1,23 @@ - + + अद्याप कोणतीही नोंद नाही + अद्याप कोणताही तपशील नाही + + ॲप आवृत्ती %1$s + तुम्हाला लागणारा एकमेव पासवर्ड व्यवस्थापक + + + + वगळा + पुढे + तयार करा\n[[सुरक्षित]]\nपासवर्ड. + तुमचे सर्व\n[[पासवर्ड]]\nइथेच आहेत. + टाइप नको,\nमाहिती\n[[ऑटोफिल]] करा. + तुमच्या ऑनलाइन खात्यांसाठी असुरक्षित पासवर्ड वापरणे थांबवा, PassKey सोबत पुढे जा. सर्वात सुरक्षित आणि तोडायला कठीण पासवर्ड मिळवा. + तुमचे सर्व पासवर्ड एकाच ठिकाणी साठवा आणि व्यवस्थापित करा. शेकडो पासवर्ड नको, फक्त एकच लक्षात ठेवा. + सार्वजनिक ठिकाणी टाइप करून तुमचे पासवर्ड धोक्यात आणू नका, PassKey ला ते ऑटोफिल करू द्या आणि तुमची माहिती सुरक्षित ठेवा. + बँक पत्र @@ -20,7 +38,6 @@ डेटा निर्यात करा - येथे टाईप करा… आपले शीर्षक येथे टाईप करा… आपला प्रतिसाद येथे टाईप करा… आपले शीर्षक येथे टाईप करा… @@ -35,12 +52,7 @@ पासकी मॅनेजरमध्ये आपले स्वागत आहे - प्रमाणीकरण अयशस्वी झाले. काहीतरी चुकलं आहे.. - कृपया प्रथम आपल्या डिव्हाइससाठी संकेतशब्द सेट करा. - डेटाबेस निर्यात करण्यासाठी स्टोरेज प्रवेश आवश्यक आहे. - परवानगी नकारली - ठीक आहे बदला @@ -62,15 +74,11 @@ सुरक्षा स्थिती सुरक्षा विश्लेषण - सुरक्षा स्कोअर कमकुवत पासवर्ड पुन्हा वापरलेले पासवर्ड - सुरक्षित पासवर्ड उत्कृष्ट लक्ष देणे आवश्यक आहे गंभीर - %1$d कमकुवत पासवर्ड आढळले - %1$d पुन्हा वापरलेले पासवर्ड आढळले %1$d वेळा वापरले कोणतीही समस्या आढळली नाही. छान! \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index ab5a4b8..f39b7b3 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,5 +1,23 @@ - + + Nenhum item ainda + Nenhum detalhe ainda + + Versão do app %1$s + O único gerenciador de senhas que você vai precisar + + + + Pular + Avançar + Crie\nsenhas\n[[seguras]]. + Todas as suas\n[[senhas]]\nestão aqui. + Não digite,\n[[preencha]]\nsuas credenciais. + Pare de usar senhas inseguras nas suas contas online e avance com o PassKey. Tenha senhas muito seguras e difíceis de quebrar. + Guarde e gerencie todas as suas senhas em um só lugar. Não decore centenas de senhas, memorize apenas uma. + Não comprometa suas senhas digitando-as em público; deixe o PassKey preencher tudo automaticamente e manter suas credenciais seguras. + Bancos Correios @@ -20,7 +38,6 @@ Exportar Dados - Digite aqui… Digite seu título aqui… Digite sua resposta aqui… Digite seu título aqui… @@ -35,12 +52,7 @@ Bem-vindo ao Gerenciador de Senhas - Autenticação Falhou. Algo deu errado.. - Por favor, defina uma senha para o seu dispositivo primeiro. - Acesso ao armazenamento é necessário para exportar o banco de dados. - Permissão Negada - OK Alterar @@ -62,15 +74,11 @@ Status de Segurança Análise de Segurança - Pontuação de Segurança Senhas Fracas Senhas Reutilizadas - Senhas Seguras Excelente Requer Atenção Crítico - %1$d senhas fracas encontradas - %1$d senhas reutilizadas encontradas Usado %1$d vezes Nenhum problema encontrado. Bom trabalho! \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 04dd34d..a2e5db9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,5 +1,23 @@ - + + Пока нет записей + Пока нет данных + + Версия приложения %1$s + Единственный менеджер паролей, который вам нужен + + + + Пропустить + Далее + Создавайте\n[[надёжные]]\nпароли. + Все ваши\n[[пароли]]\nздесь. + Не печатайте,\n[[заполняйте]]\nданные сами. + Перестаньте использовать ненадёжные пароли для своих аккаунтов — переходите на PassKey. Получайте самые надёжные и трудные для взлома пароли. + Храните все пароли и управляйте ими в одном месте. Запоминайте не сотни паролей, а всего один. + Не рискуйте паролями, вводя их на виду у других — позвольте PassKey заполнять их автоматически и хранить ваши данные в безопасности. + Банки Почта @@ -20,7 +38,6 @@ Экспортировать данные - Введите здесь… Введите ваш заголовок здесь… Введите ваш ответ здесь… Введите ваш заголовок здесь… @@ -35,12 +52,7 @@ Добро пожаловать в Менеджер паролей - Ошибка аутентификации. Что-то пошло не так.. - Пожалуйста, сначала установите пароль для вашего устройства. - Требуется доступ к хранилищу для экспорта базы данных. - Доступ запрещен - OK Изменить @@ -62,15 +74,11 @@ Состояние безопасности Анализ безопасности - Оценка безопасности Слабые пароли Повторяющиеся пароли - Надежные пароли Отлично Требует внимания Критично - Найдено %1$d слабых паролей - Найдено %1$d повторяющихся паролей Использовано %1$d раз Проблем не обнаружено. Отличная работа! diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 51f1619..aafd990 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,5 +1,23 @@ - + + இன்னும் உள்ளீடுகள் இல்லை + இன்னும் விவரங்கள் இல்லை + + ஆப் பதிப்பு %1$s + உங்களுக்குத் தேவையான ஒரே கடவுச்சொல் மேலாளர் + + + + தவிர் + அடுத்து + [[பாதுகாப்பான]]\nகடவுச்சொற்களை\nஉருவாக்கு. + உங்கள் அனைத்து\n[[கடவுச்சொற்களும்]]\nஇங்கே. + தட்டச்சு வேண்டாம்,\nதகவலை\n[[தானாக நிரப்பு]]. + உங்கள் ஆன்லைன் கணக்குகளுக்குப் பாதுகாப்பற்ற கடவுச்சொற்களைப் பயன்படுத்துவதை நிறுத்துங்கள், PassKey உடன் முன்னேறுங்கள். மிகவும் பாதுகாப்பான, உடைக்கக் கடினமான கடவுச்சொற்களைப் பெறுங்கள். + உங்கள் அனைத்துக் கடவுச்சொற்களையும் ஒரே இடத்தில் சேமித்து நிர்வகியுங்கள். நூற்றுக்கணக்கானவை அல்ல, ஒன்றை மட்டும் நினைவில் வையுங்கள். + பொது இடத்தில் தட்டச்சு செய்து உங்கள் கடவுச்சொற்களை ஆபத்தில் ஆழ்த்த வேண்டாம், PassKey அவற்றைத் தானாக நிரப்பி உங்கள் தகவலைப் பாதுகாக்கட்டும். + வங்கிகள் மின்னஞ்சல்கள் @@ -20,7 +38,6 @@ தரவு ஏற்றுமதி - இங்கே தட்டச்சு செய்யவும்… உங்கள் தலைப்பு இங்கே தட்டச்சு செய்யவும்… உங்கள் பதில் இங்கே தட்டச்சு செய்யவும்… உங்கள் தலைப்பு இங்கே தட்டச்சு செய்யவும்… @@ -35,12 +52,7 @@ பாஸ்கீ மேனேஜருக்கு வரவேற்கிறோம் - அங்கீகாரம் தோல்வியடைந்தது. எதாவது தவறு நடந்தது.. - தயவுசெய்து முதலில் உங்கள் சான்றுகளுக்கு கடவுச்சொல் அமைக்கவும். - தரவுத்தள ஏற்றுமதி செய்யும் கடன் பெற சேமிக்கும் அணுகல் தேவைப்படுகிறது. - அனுமதி நிராகரிக்கப்பட்டது - சரி மாற்று @@ -62,15 +74,11 @@ பாதுகாப்பு நிலை பாதுகாப்பு பகுப்பாய்வு - பாதுகாப்பு மதிப்பெண் பலவீனமான கடவுச்சொற்கள் மீண்டும் பயன்படுத்தப்பட்ட கடவுச்சொற்கள் - பாதுகாப்பான கடவுச்சொற்கள் சிறந்தது கவனம் தேவை மிக முக்கியம் - %1$d பலவீனமான கடவுச்சொற்கள் கண்டறியப்பட்டன - %1$d மீண்டும் பயன்படுத்தப்பட்ட கடவுச்சொற்கள் கண்டறியப்பட்டன %1$d முறை பயன்படுத்தப்பட்டது எந்த பிரச்சனையும் இல்லை. நன்று! diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index d209ba3..ea7126f 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -1,5 +1,23 @@ - + + ఇంకా ఎంట్రీలు లేవు + ఇంకా వివరాలు లేవు + + యాప్ వెర్షన్ %1$s + మీకు కావలసిన ఏకైక పాస్‌వర్డ్ మేనేజర్ + + + + దాటవేయి + తదుపరి + [[సురక్షిత]]\nపాస్‌వర్డ్‌లను\nసృష్టించండి. + మీ అన్ని\n[[పాస్‌వర్డ్‌లు]]\nఇక్కడే. + టైప్ చేయవద్దు,\nవివరాలను\n[[ఆటోఫిల్]] చేయండి. + మీ ఆన్‌లైన్ ఖాతాలకు అసురక్షిత పాస్‌వర్డ్‌లను ఉపయోగించడం ఆపండి, PassKey తో ముందుకు సాగండి. అత్యంత సురక్షితమైన, ఛేదించడం కష్టమైన పాస్‌వర్డ్‌లను పొందండి. + మీ అన్ని పాస్‌వర్డ్‌లను ఒకే చోట నుండి భద్రపరచండి, నిర్వహించండి. వందల పాస్‌వర్డ్‌లు కాదు, ఒక్కటే గుర్తుంచుకోండి. + బహిరంగంగా టైప్ చేసి మీ పాస్‌వర్డ్‌లను ప్రమాదంలో పెట్టవద్దు, PassKey వాటిని ఆటోఫిల్ చేసి మీ వివరాలను సురక్షితంగా ఉంచనివ్వండి. + బ్యాంకులు మెయిల్‌లు @@ -20,7 +38,6 @@ డేటా ఎగురుతున్నది - ఇక్కడ టైప్ చేయండి… మీ శీర్షికను ఇక్కడ టైప్ చేయండి… మీ సమాధానం ఇక్కడ టైప్ చేయండి… మీ శీర్షికను ఇక్కడ టైప్ చేయండి… @@ -35,12 +52,7 @@ పాస్‌కీ మేనేజర్‌కు స్వాగతం - ప్రమాణీకరణ విఫలమైంది. ఏదో తప్పు జరిగింది.. - దయచేసి మీ ఉపకరణానికి మొదటిసారిగా పాస్‌వర్డ్‌ను సెట్ చేయండి. - పరిమితితములు యిక్కడి ప్రవేశించి డేటాబేస్‌ను ఎగురుతున్నాయి. - అనుమతి తప్పబడింది - సరే మార్చు @@ -62,15 +74,11 @@ భద్రతా స్థితి భద్రతా విశ్లేషణ - భద్రతా స్కోర్ బలహీనమైన పాస్‌వర్డ్‌లు తిరిగి ఉపయోగించిన పాస్‌వర్డ్‌లు - సురక్షిత పాస్‌వర్డ్‌లు అద్భుతం శ్రద్ధ అవసరం క్లిష్టమైన - %1$d బలహీనమైన పాస్‌వర్డ్‌లు కనుగొనబడ్డాయి - %1$d తిరిగి ఉపయోగించిన పాస్‌వర్డ్‌లు కనుగొనబడ్డాయి %1$d సార్లు ఉపయోగించబడింది ఎటువంటి సమస్యలు కనుగొనబడలేదు. బాగా చేసారు! diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 8a752e4..d6594a0 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,5 +1,23 @@ - + + 暂无条目 + 暂无详情 + + 应用版本 %1$s + 你唯一需要的密码管理器 + + + + 跳过 + 下一步 + 生成\n[[安全]]\n密码。 + 你的所有\n[[密码]]\n都在这里。 + 无需输入,\n[[自动填充]]\n你的凭据。 + 别再为在线账户使用不安全的密码,用 PassKey 更进一步。获取最安全、最难破解的密码。 + 在一个地方存储和管理你的所有密码。不必记住几百个密码,只需记住一个。 + 不要在公共场合输入密码而让它们暴露风险,让 PassKey 自动填充,保护你的凭据安全。 + 银行 邮件 @@ -20,7 +38,6 @@ 导出数据 - 在此输入… 在此输入您的标题… 在此输入您的响应… 在此输入您的标题… @@ -35,12 +52,7 @@ 欢迎使用PassKey Manager - 身份验证失败。 出了点问题.. - 请先为您的设备设置密码。 - 需要访问存储空间以导出数据库。 - 权限被拒绝 - 确定 更改 @@ -62,15 +74,11 @@ 安全状态 安全分析 - 安全评分 弱密码 重复使用的密码 - 安全密码 优秀 需要注意 严重 - 发现 %1$d 个弱密码 - 发现 %1$d 个重复使用的密码 已使用 %1$d 次 未发现问题。干得好! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a043c7..9da5e95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,22 +1,36 @@ - - + - App Version %1$s + PassKey - The only password manager you\'ll ever need + App Version %1$s + The only password manager you\'ll ever need - Skip - Next - Generate\nSecure\nPasswords. - All Your\nPasswords Are\nHere. - Don\'t Type,\nAutofill Your\nCredentials. - Stop using unsecure passwords for your online accounts, level up with PassKey. Get the most secure and difficult-to-crack passwords. - Store and manage all of your passwords from one place. Don’t remember hundreds of passwords, just remember one. - Don’t compromise your passwords by typing them in public, let PassKey autofill those and keep your credentials secure. + + Skip + Next + Generate\n[[Secure]]\nPasswords. + All Your\n[[Passwords]] Are\nHere. + Don\'t Type,\n[[Autofill]] Your\nCredentials. + Stop using unsecure passwords for your online accounts, level up with PassKey. Get the most secure and difficult-to-crack passwords. + Store and manage all of your passwords from one place. Don’t remember hundreds of passwords, just remember one. + Don’t compromise your passwords by typing them in public, let PassKey autofill those and keep your credentials secure. + Welcome to Passkey: Password Manager, the ultimate solution for managing all your passwords and private information securely!\n\nAre you tired of remembering multiple passwords or annoyed of forgetting them? Passkey: Password Manager helps you store all your logins, passwords, and other private information safe and secure in an encrypted database.\n\nPasskey: Password Manager is an open-source application developed by Aditya Bhardwaj. It is designed to provide a secure way to manage all your passwords without worrying about data leaks. With Passkey: Password Manager, you can store your passwords locally on your device, and the application does not require internet permissions, making it even more secure.\n\nPasskey: Password Manager features an encrypted database to store your passwords, and biometric authentication is used to access your information. The application supports dark mode, and screenshot blocking features to ensure that your information is not accessible by unauthorized persons.\n\nThe application uses Drag & Drop and Swipe to Delete features, providing a user-friendly interface to manage all your passwords easily. The application also has a Separation of Content feature, which allows you to categorize your passwords into different sections for easy management.\n\nPasskey: Password Manager uses LiveData and MVVM architecture to ensure that the application is scalable, maintainable, and easy to use. SQL Cipher is used for Room DB Backup Encryption and Decryption, and Dagger-Hilt is used for dependency injection.\n\nIn conclusion, Passkey: Password Manager is a must-have application for everyone who wants to manage their passwords securely. With its features and user-friendly interface, you can keep all your passwords and private information safe and secure. Download Passkey: Password Manager now, and never worry about forgetting your passwords again! At Passkey: Password Manager, we are committed to protecting your privacy. This privacy policy explains how we collect, use, and share your personal information when you use our application.\n\nInformation we collect: We do not collect any of your passwords or private information that you store in the Passkey: Password Manager application. Your information is stored locally on your phone and encrypted for added security.\n\nHow we use your information: We use your information to store your passwords and private information securely in an encrypted database. We do not share your information with any third-party.\n\nHow we protect your information: We use encrypted database storage to protect your information, and biometric authentication is used to access your information.\n\nHow we share your information: We do not share your information with any third-party.\n\nChanges to our privacy policy: We reserve the right to modify this privacy policy at any time without prior notice.\n\nIf you have any questions or concerns about our terms and conditions or privacy policy, please contact us at yrkkh.cclub@gmail.com\n\nWe hope you enjoy using Passkey: Password Manager to store your passwords and private information securely. Welcome to Passkey: Password Manager! By downloading and using this application, you agree to comply with and be bound by the following terms and conditions:\n\n1. Passkey: Password Manager is an application developed by Aditya Bhardwaj, and the intellectual property rights are owned by Aditya Bhardwaj.\n\n2. The Passkey: Password Manager application is designed to store your passwords and private information securely in an encrypted database.\n\n3. The Passkey: Password Manager application does not store your credentials on servers, so your passwords and private information are in your hands.\n\n4. The Passkey: Password Manager application does not require any internet permission, and it is an open-source application.\n\n5. The Passkey: Password Manager application uses encrypted database storage to store your information, and biometric authentication is used to access your information.\n\n6. The Passkey: Password Manager application is provided "as is" without any warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose.\n\n7. Aditya Bhardwaj is not liable for any damages arising from the use of the Passkey: Password Manager application, including but not limited to direct, indirect, incidental, or consequential damages.\n\n8. By downloading and using the Passkey: Password Manager application, you agree to indemnify and hold harmless Aditya Bhardwaj, its officers, employees, agents, and affiliates from any and all claims, damages, losses, liabilities, and expenses arising from your use of the application.\n\n9. Aditya Bhardwaj reserves the right to modify, suspend, or discontinue the Passkey: Password Manager application at any time without prior notice. @@ -24,6 +38,8 @@ + No entries yet + No details yet Banks Mails Apps @@ -43,7 +59,6 @@ Export Data - Type here… Type your title here… Type your response here… Type your heading here… @@ -58,12 +73,7 @@ Welcome to PassKey Manager - Authentication Failed. Something Went Wrong.. - Please set password for your device first. - Storage access is required to export database. - Permission Denied - OK Change @@ -85,15 +95,11 @@ Safety Status Security Analysis - Security Score Weak Passwords Reused Passwords - Safe Passwords Excellent Needs Attention Critical - Found %1$d weak passwords - Found %1$d reused passwords Used %1$d times No issues found. Good job! diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index 29c6964..ec31e4d 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -16,4 +16,5 @@ + \ No newline at end of file diff --git a/app/src/test/java/com/bhardwaj/passkey/HighlightMarkupTest.kt b/app/src/test/java/com/bhardwaj/passkey/HighlightMarkupTest.kt new file mode 100644 index 0000000..2345642 --- /dev/null +++ b/app/src/test/java/com/bhardwaj/passkey/HighlightMarkupTest.kt @@ -0,0 +1,106 @@ +package com.bhardwaj.passkey + +import com.bhardwaj.passkey.presentation.screens.onboarding_screens.components.HighlightSpan +import com.bhardwaj.passkey.presentation.screens.onboarding_screens.components.parseHighlightMarkup +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Regression guard for the onboarding crash. + * + * The previous implementation looked up a hardcoded English word with `indexOf` and passed the + * result straight to `AnnotatedString.addStyle`. For any translated heading `indexOf` returned + * -1 and `addStyle(start = -1, ...)` threw. Every case below must therefore produce in-bounds + * spans and never throw. + */ +class HighlightMarkupTest { + + @Test + fun `strips markers and reports the highlighted range`() { + val result = parseHighlightMarkup("Generate\n[[Secure]]\nPasswords.") + + assertEquals("Generate\nSecure\nPasswords.", result.text) + assertEquals(listOf(HighlightSpan(9, 15)), result.spans) + assertEquals("Secure", result.text.substring(9, 15)) + } + + @Test + fun `text with no markers yields no spans`() { + val result = parseHighlightMarkup("All Your Passwords Are Here.") + + assertEquals("All Your Passwords Are Here.", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `unclosed marker is dropped rather than throwing`() { + val result = parseHighlightMarkup("Generate [[Secure Passwords.") + + assertEquals("Generate Secure Passwords.", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `stray closing marker is dropped rather than throwing`() { + val result = parseHighlightMarkup("Generate Secure]] Passwords.") + + assertEquals("Generate Secure]] Passwords.", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `empty marker pair produces no span`() { + val result = parseHighlightMarkup("Generate [[]] Passwords.") + + assertEquals("Generate Passwords.", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `supports multiple highlighted spans`() { + val result = parseHighlightMarkup("[[All]] Your [[Passwords]]") + + assertEquals("All Your Passwords", result.text) + assertEquals(listOf(HighlightSpan(0, 3), HighlightSpan(9, 18)), result.spans) + assertEquals("All", result.text.substring(0, 3)) + assertEquals("Passwords", result.text.substring(9, 18)) + } + + @Test + fun `handles markers at both string boundaries`() { + val result = parseHighlightMarkup("[[Autofill]]") + + assertEquals("Autofill", result.text) + assertEquals(listOf(HighlightSpan(0, 8)), result.spans) + } + + @Test + fun `empty input is handled`() { + val result = parseHighlightMarkup("") + + assertEquals("", result.text) + assertTrue(result.spans.isEmpty()) + } + + @Test + fun `every shipped heading parses to in-bounds spans`() { + // Representative of the real translated resources, including RTL and CJK. + val shipped = listOf( + "Generate\n[[Secure]]\nPasswords.", + "बनाएँ\n[[सुरक्षित]]\nपासवर्ड।", + "أنشئ\nكلمات مرور\n[[آمنة]].", + "[[安全な]]\nパスワードを\n作成。", + "Créez des\nmots de passe\n[[sécurisés]]." + ) + shipped.forEach { raw -> + val result = parseHighlightMarkup(raw) + assertEquals("one highlight expected in: $raw", 1, result.spans.size) + result.spans.forEach { span -> + assertTrue("start in bounds: $raw", span.start in 0..result.text.length) + assertTrue("end in bounds: $raw", span.endExclusive in 0..result.text.length) + assertTrue("non-empty span: $raw", span.endExclusive > span.start) + } + } + } +} diff --git a/app/src/test/java/com/bhardwaj/passkey/PasswordGeneratorTest.kt b/app/src/test/java/com/bhardwaj/passkey/PasswordGeneratorTest.kt new file mode 100644 index 0000000..ad3d2a6 --- /dev/null +++ b/app/src/test/java/com/bhardwaj/passkey/PasswordGeneratorTest.kt @@ -0,0 +1,102 @@ +package com.bhardwaj.passkey + +import com.bhardwaj.passkey.utils.PasswordGenerator +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Random + +class PasswordGeneratorTest { + + private companion object { + const val UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + const val LOWER = "abcdefghijklmnopqrstuvwxyz" + const val DIGITS = "0123456789" + const val SPECIAL = "!@#$%^&*()_+-=[]{}|;:,.<>?" + } + + private fun generate( + length: Int = 16, + upper: Boolean = true, + lower: Boolean = true, + numbers: Boolean = true, + special: Boolean = true, + random: Random = Random(20260916) + ) = PasswordGenerator.generate(length, upper, lower, numbers, special, random) + + @Test + fun `produces a password of the requested length`() { + for (length in 4..32) { + assertEquals(length, generate(length = length).length) + } + } + + @Test + fun `includes at least one character from every selected class`() { + // The old implementation sampled uniformly from the union, so a password could contain + // no symbol despite symbols being requested. Run many trials to catch a regression. + repeat(500) { seed -> + val password = generate(length = 8, random = Random(seed.toLong())) + assertTrue("no uppercase in $password", password.any { it in UPPER }) + assertTrue("no lowercase in $password", password.any { it in LOWER }) + assertTrue("no digit in $password", password.any { it in DIGITS }) + assertTrue("no symbol in $password", password.any { it in SPECIAL }) + } + } + + @Test + fun `never emits characters from a deselected class`() { + repeat(200) { seed -> + val password = generate( + length = 12, upper = true, lower = true, + numbers = false, special = false, random = Random(seed.toLong()) + ) + assertTrue("digit leaked into $password", password.none { it in DIGITS }) + assertTrue("symbol leaked into $password", password.none { it in SPECIAL }) + } + } + + @Test + fun `falls back to lowercase when no class is selected`() { + val password = generate( + length = 10, upper = false, lower = false, numbers = false, special = false + ) + assertEquals(10, password.length) + assertTrue("expected lowercase only, got $password", password.all { it in LOWER }) + } + + @Test + fun `length shorter than the number of selected classes is widened to fit them`() { + // Four classes requested but only two characters asked for: honouring "one per class" + // has to win, otherwise the guarantee above is a lie. + val password = generate(length = 2) + assertEquals(4, password.length) + } + + @Test + fun `consecutive calls with the real generator differ`() { + val results = List(1_000) { + PasswordGenerator.generate( + length = 16, + includeUpper = true, includeLower = true, + includeNumbers = true, includeSpecial = true + ) + } + assertEquals("generated passwords repeated", results.size, results.toSet().size) + } + + @Test + fun `first character is not always from the same class`() { + // Regression guard for emitting the per-class characters in a fixed order without + // shuffling, which would make the first character's class fully predictable. + val firstClasses = (0 until 200) + .map { generate(length = 12, random = Random(it.toLong())).first() } + .map { c -> + when (c) { + in UPPER -> "upper"; in LOWER -> "lower"; in DIGITS -> "digit"; else -> "special" + } + } + .toSet() + assertTrue("first character class never varied: $firstClasses", firstClasses.size > 1) + } +} diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 From 14e62de1bb7860789450ef1dc2e4d0bb614fa128 Mon Sep 17 00:00:00 2001 From: Aditya Bhardwaj Date: Wed, 16 Sep 2026 08:18:50 +0530 Subject: [PATCH 2/9] Upgrade toolchain to Gradle 9.7.1 / AGP 9.4.0 / Kotlin 2.3.21 Phase 0 of 5.6.0, landed on its own so the upgrade can be verified in isolation before any behavioural change is built on top of it. AGP 9 turned out to be a structural migration rather than a version bump, and two constraints forced deviations from the plan: - AGP 9 has built-in Kotlin support and rejects the 'org.jetbrains.kotlin.android' plugin outright, so that plugin is removed. Kotlin is still pinned through the compose-compiler plugin version, and the resolved compiler is 2.3.21 (verified via the buildscript classpath, which overrides AGP's bundled 2.2.10). - The plan targeted Kotlin 2.4.20, but no KSP release exists for the 2.4.x line: the newest KSP is 2.3.12, whose symbol-processing-api depends on kotlin-stdlib 2.3.20. Since Hilt and Room both run through KSP, the buildable target is the 2.3.x line. Kotlin is therefore 2.3.21, not 2.4.20. Also required, and not anticipated by the plan: - Hilt 2.57.2 failed against AGP 9 with "Android BaseExtension not found"; upgraded to 2.60.1. - androidx.hilt 1.4.0 requires compiling against API 37, so compileSdk moves 36 -> 37. targetSdk stays at 36: compiling against newer APIs does not opt the app into new runtime behaviour, and that is a separate decision. - google-services 4.5.0 and firebase-crashlytics 3.0.8 for AGP 9 compatibility. Library versions: Compose BOM 2026.09.00, core-ktx 1.19.0, activity-compose 1.13.0, appcompat 1.8.0, lifecycle 2.11.0, navigation-compose 2.10.1, coroutines 1.11.0, datastore 1.2.1, Room 2.8.5, sqlcipher-android 4.19.0, androidx.sqlite 2.7.1, reorderable 3.1.0, firebase-bom 34.19.0. Java and jvmTarget move 22 -> 21 (LTS). androidx.biometric deliberately stays at 1.1.0: despite its age it is the newest stable release, and 1.2.0/1.4.0 are alpha-only. It already supports CryptoObject, which is all the 5.7.0 key work needs, and an alpha does not belong in the auth path of a password manager. material-icons-extended is frozen upstream at 1.7.8 and is no longer BOM-managed, so it now carries an explicit version rather than resolving to nothing. Other build hygiene: - local.properties was loaded unconditionally, throwing FileNotFoundException on any machine or CI checkout without it. Now guarded. - BuildConfig.PASS_PHRASE renamed to LEGACY_PASS_PHRASE to make its purpose explicit ahead of the 5.7.0 key migration. Same value, so existing vaults still open; AppModule carries a FIXME describing the replacement. - Dropped androidx.core:core-splashscreen, which was declared but never used (installSplashScreen is never called and no splash theme references it). - Added lifecycle-runtime-compose, needed for collectAsStateWithLifecycle. - Added mockk, turbine, kotlinx-coroutines-test and a testImplementation line for truth, which was previously androidTest-only. - opencsv is retained despite being unused: Phase 3 adopts it for the legacy CSV reader. Verified: clean assembleDebug, assembleRelease and lintDebug all green, 16 unit tests pass, all 65 net.zetetic classes still kept unrenamed by the newer R8, and onboarding still renders correctly with no crash in hi and ar on an API 36 emulator. Release APK drops 28M to 13M from the newer R8. Co-Authored-By: Claude Opus 5 (1M context) --- app/build.gradle.kts | 35 ++- .../java/com/bhardwaj/passkey/di/AppModule.kt | 9 +- gradle/libs.versions.toml | 60 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 5 +- gradlew | 286 +++++++++++------- gradlew.bat | 41 +-- 7 files changed, 266 insertions(+), 170 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9625743..7de5eb2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,6 @@ import java.util.Properties plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose.compiler) alias(libs.plugins.ksp) alias(libs.plugins.hilt.android) @@ -14,7 +13,7 @@ plugins { android { namespace = "com.bhardwaj.passkey" - compileSdk = 36 + compileSdk = 37 defaultConfig { applicationId = "com.bhardwaj.passkey" @@ -25,9 +24,21 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - val properties = Properties() - properties.load(project.rootProject.file("local.properties").inputStream()) - buildConfigField("String", "PASS_PHRASE", "\"${properties.getProperty("PASS_PHRASE")}\"") + // local.properties is gitignored, so a fresh clone or CI checkout will not have it. + // Loading it unconditionally threw FileNotFoundException and broke configuration. + val localProperties = Properties().apply { + val file = project.rootProject.file("local.properties") + if (file.exists()) file.inputStream().use { load(it) } + } + // Renamed from PASS_PHRASE: this value only exists to open pre-5.7 databases that were + // encrypted with the old build-time constant, so they can be re-keyed. Never use it for + // new vaults. Note that when the key is absent the historical builds embedded the literal + // string "null", which the migration also has to treat as a candidate. + buildConfigField( + "String", + "LEGACY_PASS_PHRASE", + "\"${localProperties.getProperty("PASS_PHRASE")}\"" + ) vectorDrawables { useSupportLibrary = true @@ -58,12 +69,12 @@ android { } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_22 - targetCompatibility = JavaVersion.VERSION_22 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } kotlin { compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_22) + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) } } buildFeatures { @@ -115,6 +126,7 @@ dependencies { implementation(libs.androidx.appcompat) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.navigation.compose) // Coroutines @@ -127,9 +139,6 @@ dependencies { ksp(libs.hilt.compiler) implementation(libs.hilt.navigation.compose) - // Splash Screen - implementation(libs.androidx.core.splashscreen) - // DataStore Preferences implementation(libs.datastore.preferences) @@ -164,6 +173,10 @@ dependencies { // Testing & Debugging testImplementation(libs.junit) + testImplementation(libs.google.truth) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.mockk) + testImplementation(libs.turbine) androidTestImplementation(libs.test.ext.junit) androidTestImplementation(libs.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/java/com/bhardwaj/passkey/di/AppModule.kt b/app/src/main/java/com/bhardwaj/passkey/di/AppModule.kt index 118b320..0cd61de 100644 --- a/app/src/main/java/com/bhardwaj/passkey/di/AppModule.kt +++ b/app/src/main/java/com/bhardwaj/passkey/di/AppModule.kt @@ -30,7 +30,14 @@ object AppModule { name = Constants.PASS_KEY_DATABASE ) .addMigrations(MIGRATION_1_2) - .openHelperFactory(factory = SupportOpenHelperFactory(BuildConfig.PASS_PHRASE.toByteArray())) + // FIXME(5.7.0): this is a single build-time constant, identical for every install + // and recoverable from the APK, so the database is encrypted against a filesystem + // thief and nothing else. It is replaced in 5.7.0 by a per-install random key + // wrapped by an AndroidKeyStore key; this value then survives only to open and + // re-key pre-5.7 databases. + .openHelperFactory( + factory = SupportOpenHelperFactory(BuildConfig.LEGACY_PASS_PHRASE.toByteArray()) + ) .build() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bd519ea..3207c2b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,54 +1,52 @@ [versions] # Plugin versions -android_gradle_plugin = "8.13.1" -kotlin_plugin = "2.2.21" -hilt_plugin = "2.57.2" -ksp_plugin = "2.2.21-2.0.4" -google_services_plugin = "4.4.4" -firebase_crashlytics_plugin = "3.0.6" +android_gradle_plugin = "9.4.0" +kotlin_plugin = "2.3.21" +hilt_plugin = "2.60.1" +ksp_plugin = "2.3.12" +google_services_plugin = "4.5.0" +firebase_crashlytics_plugin = "3.0.8" firebase_perf_plugin = "2.0.2" # Dependency versions -core_ktx = "1.17.0" -activity_compose = "1.11.0" -compose_bom = "2025.11.00" -appcompat = "1.7.1" -lifecycle_runtime_ktx = "2.9.4" -lifecycle_viewmodel_compose = "2.9.4" -navigation_compose = "2.9.6" +core_ktx = "1.19.0" +activity_compose = "1.13.0" +compose_bom = "2026.09.00" +appcompat = "1.8.0" +lifecycle_runtime_ktx = "2.11.0" +lifecycle_viewmodel_compose = "2.11.0" +navigation_compose = "2.10.1" # Coroutines -coroutines_core = "1.10.2" -coroutines_android = "1.10.2" +coroutines_core = "1.11.0" +coroutines_android = "1.11.0" # Dagger - Hilt -hilt_android = "2.57.2" -hilt_compiler = "1.3.0" +hilt_android = "2.60.1" +hilt_compiler = "1.4.0" -# Splash Screen -splash_screen = "1.2.0" # DataStore Preferences -datastore_preferences = "1.1.7" +datastore_preferences = "1.2.1" # Biometric Authentication biometric = "1.1.0" # Room -room = "2.8.3" +room = "2.8.5" # SQL Cipher -sqlcipherAndroid = "4.11.0" -sqlcipherSqlite = "2.6.1" +sqlcipherAndroid = "4.19.0" +sqlcipherSqlite = "2.7.1" # Open CSV - Import/ Export Data opencsv = "5.12.0" # Reordering Lazy Columns -reorderable = "3.0.0" +reorderable = "3.1.0" # Firebase -firebase_bom = "34.6.0" +firebase_bom = "34.19.0" # In App Updates inAppUpdate = "2.1.0" @@ -58,7 +56,11 @@ junit = "4.13.2" test_ext_junit = "1.3.0" espresso_core = "3.7.0" compose_ui_test = "1.9.4" +# material-icons-extended is frozen upstream at 1.7.8 and is no longer BOM-managed. +material_icons = "1.7.8" truth = "1.4.5" +mockk = "1.14.11" +turbine = "1.2.1" [plugins] # Plugin definitions @@ -81,10 +83,11 @@ androidx_compose_ui = { module = "androidx.compose.ui:ui" } androidx_compose_ui_graphics = { module = "androidx.compose.ui:ui-graphics" } androidx_compose_ui_tooling_preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx_compose_material3 = { module = "androidx.compose.material3:material3" } -androidx_compose_material_icons_extended = { module = "androidx.compose.material:material-icons-extended" } +androidx_compose_material_icons_extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "material_icons" } androidx_appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } androidx_lifecycle_runtime_ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_runtime_ktx" } androidx_lifecycle_viewmodel_compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle_viewmodel_compose" } +androidx_lifecycle_runtime_compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle_runtime_ktx" } androidx_navigation_compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation_compose" } # Coroutines @@ -97,8 +100,6 @@ hilt_android_compiler = { module = "com.google.dagger:hilt-android-compiler", ve hilt_compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hilt_compiler" } hilt_navigation_compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hilt_compiler" } -# Splash Screen -androidx_core_splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splash_screen" } # DataStore Preferences datastore_preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore_preferences" } @@ -138,5 +139,8 @@ test_ext_junit = { module = "androidx.test.ext:junit", version.ref = "test_ext_j espresso_core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso_core" } compose_ui_test_junit4 = { module = "androidx.compose.ui:ui-test-junit4" } google_truth = { module = "com.google.truth:truth", version.ref = "truth" } +kotlinx_coroutines_test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines_core" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } compose_ui_tooling = { module = "androidx.compose.ui:ui-tooling" } compose_ui_test_manifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "compose_ui_test" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch literal 43764 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

K7UpUK$>l@ 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 E2O(~oYXATM diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 9f30b42..c42672d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,7 @@ -#Wed Aug 07 21:55:31 IST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,81 +15,115 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,88 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..5eed7ee 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,8 +13,10 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +27,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -56,32 +59,34 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 40458040d9dc24bb589ebc87cd64760fae0aecbb Mon Sep 17 00:00:00 2001 From: Aditya Bhardwaj Date: Wed, 16 Sep 2026 08:30:07 +0530 Subject: [PATCH 3/9] Resolve UI text at the call site, harden the clipboard, finish localization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes Phase 1 and brings §2.5 forward, since both rewrite the same code. ViewModels resolved strings eagerly via UiText.StringResource(...).asString(appContext). Below API 33 - which is most of this app's supported range, minSdk being 28 - AppCompatDelegate.setApplicationLocales applies the locale override per Activity only, so the injected Application context keeps the *system* locale and every string resolved there came back in the previous language after a user switched language in Settings. That is a second root cause of "the app renders in English", independent of the translatable="false" bug fixed in 5.5.2. UiEvents.ShowSnackBar now carries UiText unresolved and the screens resolve it against LocalContext, which does honour the override on every API level. UiText gains a PluralResource variant, and its subtypes are now data classes holding List rather than a class with a vararg array field, so effects compare by value - identity comparison would have silently broken the effect assertions planned for the ViewModel tests. The Add/Edit bottom sheet title was a *resolved English string* stored in SavedStateHandle. It is now a boolean the UI turns into text. Clipboard moves out of the ViewModels into a CopyToClipboard effect handled by the UI, which is also where it belongs given Android 10+ only lets a focused app write the clipboard. The new SecureClipboard sets EXTRA_IS_SENSITIVE so Android 13+ redacts the paste preview, and clears the clip after 30s - but only if the clip is still the one we wrote, checked by ClipDescription timestamp, so we never wipe something the user copied from another app meanwhile. Auto-clear is best-effort by construction and is documented as such: a delayed clear that fires while backgrounded is a silent no-op on API 29+. Copying an entry *heading* is marked non-sensitive; only the secret itself is. On Android 13+ the app's own "copied" snackbar is suppressed because the system shows its own confirmation. PreviewViewModel and DetailViewModel no longer take an Application at all. Remaining localization work from the plan: - All 19 hardcoded contentDescriptions addressed. Actionable controls get localized strings; decorative branding images get null, because a screen reader announcing "App Icon" next to a visible title is noise. - Hardcoded "Password Settings", "Length: N", "Invalid file selected." and the "$score%" percentage extracted - percent placement is locale-dependent. - First plurals resource in the project, replacing a singular-only "used N times". Quantity classes follow CLDR per language rather than mirroring English: Arabic gets six forms, Russian four, Spanish/French/Italian/Portuguese three, and Japanese/Korean/Chinese only "other". - 15 new strings plus the plural translated into all 16 locales. - Added values-night: the only theme hardcoded windowLightStatusBar=true against a Light parent, so night mode showed dark status-bar icons on a dark bar and flashed a white window on cold start. windowLightStatusBar is now unset so enableEdgeToEdge's SystemBarStyle.auto owns bar contrast, and an explicit windowBackground is defined for both modes. res/values/colors.xml was an empty and now holds those. Lint caught a genuine defect during this work: context.getString inside a composable is not configuration-aware. Hoisted into composable scope. Verified: clean debug build, 16 unit tests, and lint all green; onboarding renders correctly in Hindi with no crash in both light and dark mode on an API 36 emulator. Co-Authored-By: Claude Opus 5 (1M context) --- .../domain/viewModels/DetailViewModel.kt | 28 ++---- .../domain/viewModels/PreviewViewModel.kt | 28 ++---- .../domain/viewModels/SettingsViewModel.kt | 17 +--- .../screens/common/PasskeySearchBar.kt | 4 +- .../screens/detail_screen/DetailPage.kt | 37 ++++++-- .../components/DetailsBottomSheet.kt | 4 +- .../detail_screen/components/DetailsItem.kt | 6 +- .../components/PasswordSettingsSheet.kt | 6 +- .../screens/preview_screen/PreviewPage.kt | 39 ++++++-- .../preview_screen/components/PreviewItem.kt | 6 +- .../screens/security_screen/SecurityPage.kt | 8 +- .../screens/settings_screen/SettingsPage.kt | 15 +++- .../components/AnalysisBottomSheet.kt | 7 +- .../components/LanguageBottomSheet.kt | 2 +- .../screens/splash_screen/SplashPage.kt | 2 +- .../bhardwaj/passkey/utils/SecureClipboard.kt | 90 +++++++++++++++++++ .../com/bhardwaj/passkey/utils/UiEvents.kt | 22 ++++- .../java/com/bhardwaj/passkey/utils/UiText.kt | 59 ++++++++---- app/src/main/res/values-ar/strings.xml | 24 ++++- app/src/main/res/values-bn/strings.xml | 20 ++++- app/src/main/res/values-de/strings.xml | 20 ++++- app/src/main/res/values-es/strings.xml | 21 ++++- app/src/main/res/values-fr/strings.xml | 21 ++++- app/src/main/res/values-gu/strings.xml | 20 ++++- app/src/main/res/values-hi/strings.xml | 20 ++++- app/src/main/res/values-it/strings.xml | 21 ++++- app/src/main/res/values-ja/strings.xml | 19 +++- app/src/main/res/values-ko/strings.xml | 19 +++- app/src/main/res/values-mr/strings.xml | 20 ++++- app/src/main/res/values-night/colors.xml | 4 + app/src/main/res/values-night/themes.xml | 9 ++ app/src/main/res/values-pt/strings.xml | 21 ++++- app/src/main/res/values-ru/strings.xml | 22 ++++- app/src/main/res/values-ta/strings.xml | 20 ++++- app/src/main/res/values-te/strings.xml | 20 ++++- app/src/main/res/values-zh/strings.xml | 19 +++- app/src/main/res/values/colors.xml | 9 +- app/src/main/res/values/strings.xml | 28 +++++- app/src/main/res/values/themes.xml | 10 ++- 39 files changed, 633 insertions(+), 134 deletions(-) create mode 100644 app/src/main/java/com/bhardwaj/passkey/utils/SecureClipboard.kt create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values-night/themes.xml diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt index e95cfad..8ab2f38 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt @@ -1,9 +1,5 @@ package com.bhardwaj.passkey.domain.viewModels -import android.app.Application -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -36,7 +32,6 @@ import javax.inject.Inject @HiltViewModel class DetailViewModel @Inject constructor( private val repository: PasskeyRepository, - private val appContext: Application, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val _uiEvents = Channel() @@ -44,7 +39,8 @@ class DetailViewModel @Inject constructor( val detailTitle = savedStateHandle.getStateFlow(DETAIL_TITLE, "") val detailResponse = savedStateHandle.getStateFlow(DETAIL_RESPONSE, "") - val bottomSheetHeading = savedStateHandle.getStateFlow(BOTTOM_SHEET_HEADING, "") + /** true = editing an existing row, false = adding. Resolved to text by the UI. */ + val isEditingSheet = savedStateHandle.getStateFlow(BOTTOM_SHEET_HEADING, false) val previewId = savedStateHandle.get("previewId") ?: -1 private val _searchText = MutableStateFlow("") @@ -93,8 +89,7 @@ class DetailViewModel @Inject constructor( fun onEvent(event: DetailEvents) { when (event) { DetailEvents.OnAddDetailClick -> { - savedStateHandle[BOTTOM_SHEET_HEADING] = - UiText.StringResource(R.string.add).asString(context = appContext) + savedStateHandle[BOTTOM_SHEET_HEADING] = false isSheetOpen = true _searchText.value = "" } @@ -110,8 +105,7 @@ class DetailViewModel @Inject constructor( _searchText.value = "" repository.getDetailById(event.details.detailsId!!)?.let { detail -> isSheetOpen = true - savedStateHandle[BOTTOM_SHEET_HEADING] = - UiText.StringResource(R.string.edit).asString(context = appContext) + savedStateHandle[BOTTOM_SHEET_HEADING] = true savedStateHandle[DETAIL_TITLE] = event.details.question savedStateHandle[DETAIL_RESPONSE] = event.details.answer this@DetailViewModel.detail = detail @@ -136,7 +130,6 @@ class DetailViewModel @Inject constructor( sendUiEvents( UiEvents.ShowSnackBar( message = UiText.StringResource(R.string.enter_valid_title_n_response) - .asString(context = appContext) ) ) return@launch @@ -146,7 +139,6 @@ class DetailViewModel @Inject constructor( sendUiEvents( UiEvents.ShowSnackBar( message = UiText.StringResource(R.string.something_went_wrong) - .asString(context = appContext) ) ) return@launch @@ -177,17 +169,7 @@ class DetailViewModel @Inject constructor( } is DetailEvents.OnLongPress -> { - val clipboardManager = - appContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = - ClipData.newPlainText("Text Copied Successfully.", event.detailsDescription) - clipboardManager.setPrimaryClip(clipData) - sendUiEvents( - UiEvents.ShowSnackBar( - message = UiText.StringResource(R.string.copied) - .asString(context = appContext) - ) - ) + sendUiEvents(UiEvents.CopyToClipboard(value = event.detailsDescription, isSensitive = true)) } is DetailEvents.OnSwipedLeft -> { diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt index 8265817..d695c53 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt @@ -1,9 +1,5 @@ package com.bhardwaj.passkey.domain.viewModels -import android.app.Application -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -36,7 +32,6 @@ import javax.inject.Inject @HiltViewModel class PreviewViewModel @Inject constructor( private val repository: PasskeyRepository, - private val appContext: Application, private val savedStateHandle: SavedStateHandle ) : ViewModel() { private val _uiEvents = Channel() @@ -44,7 +39,8 @@ class PreviewViewModel @Inject constructor( val categoryName = savedStateHandle.getStateFlow(PREVIEW_CATEGORY_NAME, Categories.BANKS.name) val previewHeading = savedStateHandle.getStateFlow(PREVIEW_HEADING, "") - val bottomSheetHeading = savedStateHandle.getStateFlow(BOTTOM_SHEET_HEADING, "") + /** true = editing an existing row, false = adding. Resolved to text by the UI. */ + val isEditingSheet = savedStateHandle.getStateFlow(BOTTOM_SHEET_HEADING, false) private val _searchText = MutableStateFlow("") val searchText = _searchText.asStateFlow() @@ -82,8 +78,7 @@ class PreviewViewModel @Inject constructor( } PreviewEvents.OnAddPreviewClick -> { - savedStateHandle[BOTTOM_SHEET_HEADING] = - UiText.StringResource(R.string.add).asString(context = appContext) + savedStateHandle[BOTTOM_SHEET_HEADING] = false isSheetOpen = true _searchText.value = "" } @@ -98,8 +93,7 @@ class PreviewViewModel @Inject constructor( _searchText.value = "" repository.getPreviewById(event.preview.previewId!!)?.let { preview -> isSheetOpen = true - savedStateHandle[BOTTOM_SHEET_HEADING] = - UiText.StringResource(R.string.edit).asString(context = appContext) + savedStateHandle[BOTTOM_SHEET_HEADING] = true savedStateHandle[PREVIEW_HEADING] = event.preview.heading this@PreviewViewModel.preview = preview } @@ -117,7 +111,6 @@ class PreviewViewModel @Inject constructor( sendUiEvents( UiEvents.ShowSnackBar( message = UiText.StringResource(R.string.enter_valid_heading) - .asString(context = appContext) ) ) return@launch @@ -144,7 +137,6 @@ class PreviewViewModel @Inject constructor( sendUiEvents( UiEvents.ShowSnackBar( message = UiText.StringResource(R.string.heading_exists) - .asString(context = appContext) ) ) return@launch @@ -171,17 +163,7 @@ class PreviewViewModel @Inject constructor( } is PreviewEvents.OnLongPress -> { - val clipboardManager = - appContext.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = - ClipData.newPlainText("Text Copied Successfully.", event.previewHeading) - clipboardManager.setPrimaryClip(clipData) - sendUiEvents( - UiEvents.ShowSnackBar( - message = UiText.StringResource(R.string.copied) - .asString(context = appContext) - ) - ) + sendUiEvents(UiEvents.CopyToClipboard(value = event.previewHeading, isSensitive = false)) } PreviewEvents.OnSettingsClick -> { diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/SettingsViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/SettingsViewModel.kt index 1c4f22a..4162032 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/SettingsViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/SettingsViewModel.kt @@ -118,7 +118,6 @@ class SettingsViewModel @Inject constructor( sendUiEvents( UiEvents.ShowSnackBar( message = UiText.StringResource(R.string.coming_soon) - .asString(context = appContext) ) ) } else { @@ -148,17 +147,13 @@ class SettingsViewModel @Inject constructor( if (exportDataToStorage(fileName)) { sendUiEvents( UiEvents.ShowSnackBar( - message = UiText.StringResource( - R.string.export_download - ).asString(appContext) + message = UiText.StringResource(R.string.export_download) ) ) } else { sendUiEvents( UiEvents.ShowSnackBar( - message = UiText.StringResource( - R.string.something_went_wrong - ).asString(appContext) + message = UiText.StringResource(R.string.something_went_wrong) ) ) } @@ -170,17 +165,13 @@ class SettingsViewModel @Inject constructor( if (importDateFromStorage(event.content)) { sendUiEvents( UiEvents.ShowSnackBar( - message = UiText.StringResource( - R.string.import_success - ).asString(appContext) + message = UiText.StringResource(R.string.import_success) ) ) } else { sendUiEvents( UiEvents.ShowSnackBar( - message = UiText.StringResource( - R.string.import_failed - ).asString(appContext) + message = UiText.StringResource(R.string.import_failed) ) ) } diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/common/PasskeySearchBar.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/common/PasskeySearchBar.kt index 3602db9..2105784 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/common/PasskeySearchBar.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/common/PasskeySearchBar.kt @@ -50,7 +50,7 @@ fun PasskeySearchBar( ) { Icon( imageVector = Icons.Default.Search, - contentDescription = "Search Icon", + contentDescription = null, tint = MaterialTheme.colorScheme.outline ) @@ -89,7 +89,7 @@ fun PasskeySearchBar( interactionSource = remember { MutableInteractionSource() }, ), imageVector = Icons.Default.Close, - contentDescription = "Close Icon", + contentDescription = stringResource(R.string.cd_clear_search), tint = MaterialTheme.colorScheme.outline ) } diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt index 9e3197a..8e90792 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt @@ -37,6 +37,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource @@ -54,6 +55,9 @@ import com.bhardwaj.passkey.presentation.screens.detail_screen.components.Detail import com.bhardwaj.passkey.presentation.screens.detail_screen.components.PasswordSettingsSheet import com.bhardwaj.passkey.presentation.theme.BebasNeue import com.bhardwaj.passkey.utils.ButtonType +import com.bhardwaj.passkey.utils.asString +import com.bhardwaj.passkey.utils.SecureClipboard +import com.bhardwaj.passkey.utils.UiText import com.bhardwaj.passkey.utils.UiEvents import kotlinx.coroutines.launch import sh.calvin.reorderable.ReorderableItem @@ -67,7 +71,10 @@ fun DetailScreen( ) { val detailTitle by viewModel.detailTitle.collectAsState() val detailResponse by viewModel.detailResponse.collectAsState() - val bottomSheetHeading by viewModel.bottomSheetHeading.collectAsState() + val isEditingSheet by viewModel.isEditingSheet.collectAsState() + // Resolved here rather than in the ViewModel so it follows the app locale. + val bottomSheetHeading = + stringResource(if (isEditingSheet) R.string.edit else R.string.add) val searchText by viewModel.searchText.collectAsState() val details by viewModel.details.collectAsState(initial = emptyList()) @@ -83,6 +90,8 @@ fun DetailScreen( } val isPasswordSettingsOpen = viewModel.isPasswordSettingsOpen + val context = LocalContext.current + LaunchedEffect(key1 = true) { viewModel.uiEvents.collect { event -> when (event) { @@ -90,11 +99,27 @@ fun DetailScreen( is UiEvents.ShowSnackBar -> { scope.launch { snackBarHostState.showSnackbar( - message = event.message, - actionLabel = event.action + message = event.message.asString(context), + actionLabel = event.action?.asString(context) ) } } + is UiEvents.CopyToClipboard -> { + SecureClipboard.copy( + context = context, + text = event.value, + isSensitive = event.isSensitive + ) + // Android 13+ shows its own copy confirmation, so a second one is noise. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + scope.launch { + snackBarHostState.showSnackbar( + message = UiText.StringResource(R.string.copied).asString(context) + ) + } + } + } + else -> Unit } @@ -113,7 +138,7 @@ fun DetailScreen( content = { Icon( imageVector = Icons.Default.Add, - contentDescription = "Add Detail", + contentDescription = stringResource(R.string.cd_add_detail), ) }, ) @@ -129,7 +154,7 @@ fun DetailScreen( Column(modifier = Modifier.fillMaxSize()) { Icon( painter = painterResource(id = R.drawable.icon_logo), - contentDescription = "App Icon", + contentDescription = null, tint = MaterialTheme.colorScheme.primary ) Text( @@ -201,7 +226,7 @@ fun DetailScreen( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Icon Delete", + contentDescription = stringResource(R.string.cd_delete), modifier = Modifier.align(Alignment.CenterEnd), tint = MaterialTheme.colorScheme.background ) diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsBottomSheet.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsBottomSheet.kt index 5e0c616..6f49db2 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsBottomSheet.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsBottomSheet.kt @@ -131,7 +131,7 @@ fun DetailsBottomSheet( ) { Icon( imageVector = Icons.Default.Key, - contentDescription = "Refresh", + contentDescription = stringResource(R.string.cd_generate_password), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -148,7 +148,7 @@ fun DetailsBottomSheet( ) { Icon( imageVector = Icons.Default.Settings, - contentDescription = "Settings", + contentDescription = stringResource(R.string.cd_password_options), tint = MaterialTheme.colorScheme.onSurfaceVariant ) } diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsItem.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsItem.kt index e4cfa15..f7b6ff6 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsItem.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsItem.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView @@ -31,6 +32,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.bhardwaj.passkey.R import com.bhardwaj.passkey.data.local.entity.Details import com.bhardwaj.passkey.domain.events.DetailEvents import com.bhardwaj.passkey.presentation.theme.BebasNeue @@ -83,7 +85,7 @@ fun DetailsItem( } } ?: Modifier, imageVector = Icons.Default.Apps, - contentDescription = "Icon Drag", + contentDescription = stringResource(R.string.cd_drag_handle), tint = MaterialTheme.colorScheme.primary ) Text( @@ -115,7 +117,7 @@ fun DetailsItem( interactionSource = remember { MutableInteractionSource() } ), imageVector = Icons.Default.Edit, - contentDescription = "Icon Edit", + contentDescription = stringResource(R.string.cd_edit), tint = MaterialTheme.colorScheme.primary ) } diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/PasswordSettingsSheet.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/PasswordSettingsSheet.kt index add7399..339eeee 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/PasswordSettingsSheet.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/PasswordSettingsSheet.kt @@ -20,10 +20,12 @@ import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.bhardwaj.passkey.R import com.bhardwaj.passkey.presentation.theme.BebasNeue import com.bhardwaj.passkey.presentation.theme.Poppins @@ -55,7 +57,7 @@ fun PasswordSettingsSheet( .padding(bottom = 32.dp) ) { Text( - text = "Password Settings", + text = stringResource(R.string.password_settings), fontFamily = BebasNeue, fontSize = 32.sp, color = MaterialTheme.colorScheme.secondary, @@ -63,7 +65,7 @@ fun PasswordSettingsSheet( ) Text( - text = "Length: ${length.toInt()}", + text = stringResource(R.string.password_length, length.toInt()), fontFamily = Poppins, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onBackground diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt index 5e48793..11304b4 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt @@ -48,6 +48,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource @@ -66,6 +67,9 @@ import com.bhardwaj.passkey.presentation.screens.preview_screen.components.Previ import com.bhardwaj.passkey.presentation.theme.BebasNeue import com.bhardwaj.passkey.utils.ButtonType import com.bhardwaj.passkey.utils.Categories +import com.bhardwaj.passkey.utils.asString +import com.bhardwaj.passkey.utils.SecureClipboard +import com.bhardwaj.passkey.utils.UiText import com.bhardwaj.passkey.utils.UiEvents import kotlinx.coroutines.launch import sh.calvin.reorderable.ReorderableItem @@ -83,7 +87,10 @@ fun PreviewScreen( val categoryNameMap = Categories.entries.associate { it.name to stringResource(it.labelRes) } val previewHeading by viewModel.previewHeading.collectAsState() - val bottomSheetHeading by viewModel.bottomSheetHeading.collectAsState() + val isEditingSheet by viewModel.isEditingSheet.collectAsState() + // Resolved here rather than in the ViewModel so it follows the app locale. + val bottomSheetHeading = + stringResource(if (isEditingSheet) R.string.edit else R.string.add) val searchText by viewModel.searchText.collectAsState() val previews by viewModel.previews.collectAsState() @@ -100,6 +107,8 @@ fun PreviewScreen( } } + val context = LocalContext.current + LaunchedEffect(key1 = true) { viewModel.uiEvents.collect { event -> when (event) { @@ -107,11 +116,27 @@ fun PreviewScreen( is UiEvents.ShowSnackBar -> { scope.launch { snackBarHostState.showSnackbar( - message = event.message, - actionLabel = event.action + message = event.message.asString(context), + actionLabel = event.action?.asString(context) ) } } + is UiEvents.CopyToClipboard -> { + SecureClipboard.copy( + context = context, + text = event.value, + isSensitive = event.isSensitive + ) + // Android 13+ shows its own copy confirmation, so a second one is noise. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + scope.launch { + snackBarHostState.showSnackbar( + message = UiText.StringResource(R.string.copied).asString(context) + ) + } + } + } + else -> Unit } @@ -143,7 +168,7 @@ fun PreviewScreen( content = { Icon( imageVector = Icons.Default.Add, - contentDescription = "Add Preview", + contentDescription = stringResource(R.string.cd_add_entry), ) } ) @@ -164,12 +189,12 @@ fun PreviewScreen( ) { Icon( painter = painterResource(id = R.drawable.icon_logo), - contentDescription = "App Icon", + contentDescription = null, tint = MaterialTheme.colorScheme.primary ) Icon( imageVector = Icons.Default.Settings, - contentDescription = "Settings Icon", + contentDescription = stringResource(R.string.cd_settings), tint = MaterialTheme.colorScheme.primary, modifier = Modifier .padding(8.dp) @@ -253,7 +278,7 @@ fun PreviewScreen( ) { Icon( imageVector = Icons.Default.Delete, - contentDescription = "Icon Delete", + contentDescription = stringResource(R.string.cd_delete), modifier = Modifier.align(Alignment.CenterEnd), tint = MaterialTheme.colorScheme.background ) diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewItem.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewItem.kt index ffc9739..4e6b710 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewItem.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewItem.kt @@ -23,12 +23,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.bhardwaj.passkey.R import com.bhardwaj.passkey.domain.events.PreviewEvents import com.bhardwaj.passkey.presentation.theme.Poppins import sh.calvin.reorderable.ReorderableCollectionItemScope @@ -76,7 +78,7 @@ fun PreviewItem( } } ?: Modifier, imageVector = Icons.Default.Apps, - contentDescription = "Icon Drag", + contentDescription = stringResource(R.string.cd_drag_handle), tint = MaterialTheme.colorScheme.primary ) Text( @@ -110,7 +112,7 @@ fun PreviewItem( interactionSource = remember { MutableInteractionSource() } ), imageVector = Icons.Default.Edit, - contentDescription = "Icon Edit", + contentDescription = stringResource(R.string.cd_edit), tint = MaterialTheme.colorScheme.primary ) } diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/security_screen/SecurityPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/security_screen/SecurityPage.kt index 2113a7b..314207e 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/security_screen/SecurityPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/security_screen/SecurityPage.kt @@ -53,6 +53,7 @@ import com.bhardwaj.passkey.R import com.bhardwaj.passkey.domain.events.SecurityEvents import com.bhardwaj.passkey.domain.viewModels.SecurityViewModel import com.bhardwaj.passkey.presentation.theme.Poppins +import com.bhardwaj.passkey.utils.asString import com.bhardwaj.passkey.utils.UiEvents import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -65,6 +66,8 @@ fun SecurityScreen( val snackBarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() + val context = LocalContext.current + LaunchedEffect(key1 = true) { viewModel.uiEvents.collect { event -> when (event) { @@ -76,8 +79,8 @@ fun SecurityScreen( is UiEvents.ShowSnackBar -> { scope.launch { snackBarHostState.showSnackbar( - message = event.message, - actionLabel = event.action + message = event.message.asString(context), + actionLabel = event.action?.asString(context) ) } } @@ -88,7 +91,6 @@ fun SecurityScreen( } val activity = LocalActivity.current as FragmentActivity - val context = LocalContext.current val startForResult = rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == FragmentActivity.RESULT_CANCELED) { diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/SettingsPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/SettingsPage.kt index fa2f81c..2335c78 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/SettingsPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/SettingsPage.kt @@ -49,6 +49,7 @@ import com.bhardwaj.passkey.presentation.screens.settings_screen.components.Sett import com.bhardwaj.passkey.presentation.theme.BebasNeue import com.bhardwaj.passkey.presentation.theme.Poppins import com.bhardwaj.passkey.utils.Constants.Companion.FILE_TYPE +import com.bhardwaj.passkey.utils.asString import com.bhardwaj.passkey.utils.UiEvents import kotlinx.coroutines.launch @@ -64,6 +65,8 @@ fun SettingsScreen( val scaffoldState = rememberBottomSheetScaffoldState() val context = LocalContext.current + val invalidFileMessage = stringResource(R.string.invalid_file_selected) + val importDataLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenDocument() ) { document -> @@ -75,7 +78,9 @@ fun SettingsScreen( } } else { scope.launch { - snackBarHostState.showSnackbar(message = "Invalid file selected.") + snackBarHostState.showSnackbar( + message = invalidFileMessage + ) } } } @@ -92,11 +97,13 @@ fun SettingsScreen( is UiEvents.ShowSnackBar -> { scope.launch { snackBarHostState.showSnackbar( - message = event.message, - actionLabel = event.action + message = event.message.asString(context), + actionLabel = event.action?.asString(context) ) } } + + else -> Unit } } } @@ -154,7 +161,7 @@ fun SettingsScreen( ) { Icon( painter = painterResource(id = R.drawable.icon_logo), - contentDescription = "App Icon", + contentDescription = null, tint = MaterialTheme.colorScheme.primary ) Text( diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/AnalysisBottomSheet.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/AnalysisBottomSheet.kt index bd0c688..d736d32 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/AnalysisBottomSheet.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/AnalysisBottomSheet.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp @@ -202,7 +203,7 @@ fun ScoreCircularIndicator( } Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = "$score%", + text = stringResource(R.string.security_score_percent, score), fontSize = 28.sp, fontWeight = FontWeight.Bold, fontFamily = Poppins, @@ -281,7 +282,7 @@ fun SimpleAnalysisItem(detail: Details, onClick: () -> Unit) { } Icon( imageVector = Icons.AutoMirrored.Rounded.OpenInNew, - contentDescription = "Open", + contentDescription = stringResource(R.string.cd_open), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(20.dp) ) @@ -307,7 +308,7 @@ fun ReusedAnalysisItem( modifier = Modifier.padding(bottom = 8.dp) ) { Text( - text = stringResource(R.string.used_times, count), + text = pluralStringResource(R.plurals.password_used_times, count, count), fontFamily = Poppins, fontWeight = FontWeight.Bold, fontSize = 14.sp, diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/LanguageBottomSheet.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/LanguageBottomSheet.kt index 56bc036..26eebc8 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/LanguageBottomSheet.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/LanguageBottomSheet.kt @@ -182,7 +182,7 @@ fun LanguageBottomSheet( onClick = { onBackIconClick() } ), imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back Icon" + contentDescription = stringResource(R.string.cd_back) ) Text( modifier = Modifier.padding(start = 16.dp), diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/splash_screen/SplashPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/splash_screen/SplashPage.kt index a62715d..af58f46 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/splash_screen/SplashPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/splash_screen/SplashPage.kt @@ -77,7 +77,7 @@ fun SplashPage( ) { Image( painter = painterResource(id = R.drawable.splash_logo), - contentDescription = "App Logo", + contentDescription = null, modifier = Modifier .fillMaxSize() .scale(scale.value) diff --git a/app/src/main/java/com/bhardwaj/passkey/utils/SecureClipboard.kt b/app/src/main/java/com/bhardwaj/passkey/utils/SecureClipboard.kt new file mode 100644 index 0000000..a40d2fd --- /dev/null +++ b/app/src/main/java/com/bhardwaj/passkey/utils/SecureClipboard.kt @@ -0,0 +1,90 @@ +package com.bhardwaj.passkey.utils + +import android.content.ClipData +import android.content.ClipDescription +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import android.os.PersistableBundle +import androidx.core.content.getSystemService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * Clipboard access hardened for secret values. + * + * Two problems with the previous inline `ClipData.newPlainText(...)` calls: + * - the clip carried no sensitivity flag, so Android 13+ rendered the copied password in the + * system paste preview; + * - nothing ever cleared it, so a password sat on the clipboard indefinitely. + * + * Auto-clear is best-effort by design. Since API 29 an app without window focus cannot write + * the clipboard, so a delayed clear that fires while backgrounded is a silent no-op. The real + * defences are [EXTRA_IS_SENSITIVE] and Android 12+'s own system-wide clipboard expiry; the + * timer just shortens the window while the user is still in the app. + */ +object SecureClipboard { + + /** + * Literal rather than [ClipDescription.EXTRA_IS_SENSITIVE] (API 33) on purpose: the same key + * is honoured by Gboard and several OEM clipboard managers below 33, and is a harmless no-op + * everywhere else. + */ + private const val EXTRA_IS_SENSITIVE = "android.content.extra.IS_SENSITIVE" + + private const val CLIP_LABEL = "PassKey" + const val DEFAULT_AUTO_CLEAR_MILLIS = 30_000L + + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private var clearJob: Job? = null + + /** Timestamp of the clip we last wrote, so we never wipe something the user copied since. */ + private var ourClipTimestamp: Long = 0L + + fun copy( + context: Context, + text: String, + isSensitive: Boolean, + autoClearMillis: Long = if (isSensitive) DEFAULT_AUTO_CLEAR_MILLIS else 0L + ) { + val clipboard = context.getSystemService() ?: return + + val clip = ClipData.newPlainText(CLIP_LABEL, text) + if (isSensitive) { + clip.description.extras = PersistableBundle().apply { + putBoolean(EXTRA_IS_SENSITIVE, true) + } + } + clipboard.setPrimaryClip(clip) + ourClipTimestamp = clipboard.primaryClipDescription?.timestamp ?: 0L + + clearJob?.cancel() + if (autoClearMillis > 0L) { + clearJob = scope.launch { + delay(autoClearMillis) + clearIfOurs(context) + } + } + } + + /** Clears the clipboard only if it still holds the clip this object wrote. */ + fun clearIfOurs(context: Context) { + val clipboard = context.getSystemService() ?: return + val description = clipboard.primaryClipDescription ?: return + if (description.label != CLIP_LABEL) return + if (ourClipTimestamp != 0L && description.timestamp != ourClipTimestamp) return + + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + clipboard.clearPrimaryClip() + } else { + clipboard.setPrimaryClip(ClipData.newPlainText("", "")) + } + } + ourClipTimestamp = 0L + } +} diff --git a/app/src/main/java/com/bhardwaj/passkey/utils/UiEvents.kt b/app/src/main/java/com/bhardwaj/passkey/utils/UiEvents.kt index 47bd8ca..599fcd2 100644 --- a/app/src/main/java/com/bhardwaj/passkey/utils/UiEvents.kt +++ b/app/src/main/java/com/bhardwaj/passkey/utils/UiEvents.kt @@ -2,9 +2,25 @@ package com.bhardwaj.passkey.utils sealed interface UiEvents { data object PopBackStack : UiEvents + data class Navigate(val route: String) : UiEvents + + /** + * Carries [UiText] rather than a resolved String so the message is localized at the call + * site, using the Activity context that actually honours the per-app locale. + */ data class ShowSnackBar( - val message: String, - val action: String? = null + val message: UiText, + val action: UiText? = null + ) : UiEvents + + /** + * Copying is a UI concern and needs an Activity context to satisfy Android 10+'s rule that + * only a focused app may write the clipboard, so it is an effect rather than something the + * ViewModel does itself. + */ + data class CopyToClipboard( + val value: String, + val isSensitive: Boolean ) : UiEvents -} \ No newline at end of file +} diff --git a/app/src/main/java/com/bhardwaj/passkey/utils/UiText.kt b/app/src/main/java/com/bhardwaj/passkey/utils/UiText.kt index ff640fa..769c94f 100644 --- a/app/src/main/java/com/bhardwaj/passkey/utils/UiText.kt +++ b/app/src/main/java/com/bhardwaj/passkey/utils/UiText.kt @@ -1,26 +1,51 @@ package com.bhardwaj.passkey.utils import android.content.Context +import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.compose.runtime.Composable +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -sealed class UiText { - data class DynamicString(val value: String) : UiText() - class StringResource(@StringRes val resId: Int, vararg val args: Any) : UiText() +/** + * A piece of user-facing text that is resolved as late as possible. + * + * ViewModels emit these unresolved. Resolving in a ViewModel means resolving against the + * Application context, whose Resources do not track the per-app locale override applied by + * [androidx.appcompat.app.AppCompatDelegate.setApplicationLocales] below API 33 - so on this + * app's minSdk 28..32 range every such string came back in the wrong language after the user + * switched language in Settings. + * + * These are data classes on purpose: tests assert on emitted effects by value, which an + * identity-based class (or a `vararg` array field) would silently break. + */ +sealed interface UiText { - fun asString(context: Context): String { - return when (this) { - is DynamicString -> value - is StringResource -> context.getString(resId, *args) - } - } + data class DynamicString(val value: String) : UiText - @Composable - fun asString(): String { - return when (this) { - is DynamicString -> value - is StringResource -> stringResource(resId, *args) - } - } -} \ No newline at end of file + data class StringResource( + @param:StringRes val resId: Int, + val args: List = emptyList() + ) : UiText + + data class PluralResource( + @param:PluralsRes val resId: Int, + val count: Int, + val args: List = emptyList() + ) : UiText +} + +/** Resolves against [context]. Prefer the `@Composable` overload inside composition. */ +fun UiText.asString(context: Context): String = when (this) { + is UiText.DynamicString -> value + is UiText.StringResource -> context.getString(resId, *args.toTypedArray()) + is UiText.PluralResource -> + context.resources.getQuantityString(resId, count, *args.toTypedArray()) +} + +@Composable +fun UiText.asString(): String = when (this) { + is UiText.DynamicString -> value + is UiText.StringResource -> stringResource(resId, *args.toTypedArray()) + is UiText.PluralResource -> pluralStringResource(resId, count, *args.toTypedArray()) +} diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 4ed2f7c..3cf43db 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -79,6 +79,28 @@ ممتاز يحتاج إلى انتباه حرج - مستخدمة %1$d مرات لم يتم العثور على مشاكل. أحسنت! + إعادة ترتيب + تعديل + حذف + إضافة تفصيل + إضافة إدخال + الإعدادات + إنشاء كلمة مرور + خيارات كلمة المرور + مسح البحث + فتح + رجوع + إعدادات كلمة المرور + الطول: %1$d + %1$d%% + الملف المحدد غير صالح. + + لم تُستخدم + استُخدمت مرة واحدة + استُخدمت مرتين + استُخدمت %1$d مرات + استُخدمت %1$d مرة + استُخدمت %1$d مرة + \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 6a115fb..fa41bb1 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -79,6 +79,24 @@ চমৎকার মনোযোগ প্রয়োজন জটিল - %1$d বার ব্যবহৃত হয়েছে কোনো সমস্যা পাওয়া যায়নি। দারুণ! + পুনরায় সাজান + সম্পাদনা + মুছুন + বিবরণ যোগ করুন + এন্ট্রি যোগ করুন + সেটিংস + পাসওয়ার্ড তৈরি করুন + পাসওয়ার্ড বিকল্প + অনুসন্ধান মুছুন + খুলুন + ফিরে যান + পাসওয়ার্ড সেটিংস + দৈর্ঘ্য: %1$d + %1$d%% + অবৈধ ফাইল নির্বাচিত হয়েছে। + + %1$d বার ব্যবহৃত + %1$d বার ব্যবহৃত + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8e413f5..8245b44 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -79,6 +79,24 @@ Ausgezeichnet Aufmerksamkeit erforderlich Kritisch - %1$d mal verwendet Keine Probleme gefunden. Gut gemacht! + Neu anordnen + Bearbeiten + Löschen + Detail hinzufügen + Eintrag hinzufügen + Einstellungen + Passwort generieren + Passwortoptionen + Suche löschen + Öffnen + Zurück + Passworteinstellungen + Länge: %1$d + %1$d %% + Ungültige Datei ausgewählt. + + %1$d Mal verwendet + %1$d Mal verwendet + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 447b9ee..ffff14a 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -79,6 +79,25 @@ Excelente Necesita atención Crítico - Usado %1$d veces No se encontraron problemas. ¡Bien hecho! + Reordenar + Editar + Eliminar + Añadir detalle + Añadir entrada + Ajustes + Generar contraseña + Opciones de contraseña + Borrar búsqueda + Abrir + Atrás + Ajustes de contraseña + Longitud: %1$d + %1$d %% + Archivo seleccionado no válido. + + Usada %1$d vez + Usada %1$d veces + Usada %1$d veces + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f95faa4..50024fb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -79,6 +79,25 @@ Excellent Nécessite une attention Critique - Utilisé %1$d fois Aucun problème trouvé. Bien joué ! + Réorganiser + Modifier + Supprimer + Ajouter un détail + Ajouter une entrée + Paramètres + Générer un mot de passe + Options du mot de passe + Effacer la recherche + Ouvrir + Retour + Paramètres du mot de passe + Longueur : %1$d + %1$d %% + Fichier sélectionné non valide. + + Utilisé %1$d fois + Utilisé %1$d fois + Utilisé %1$d fois + diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index 5e7bedd..d82a7f6 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -79,6 +79,24 @@ ઉત્તમ ધ્યાન આપવાની જરૂર છે ગંભીર - %1$d વખત ઉપયોગ થયો કોઈ સમસ્યા મળી નથી. સરસ! + ફરીથી ગોઠવો + સંપાદિત કરો + કાઢી નાખો + વિગત ઉમેરો + એન્ટ્રી ઉમેરો + સેટિંગ્સ + પાસવર્ડ બનાવો + પાસવર્ડ વિકલ્પો + શોધ સાફ કરો + ખોલો + પાછળ + પાસવર્ડ સેટિંગ્સ + લંબાઈ: %1$d + %1$d%% + અમાન્ય ફાઇલ પસંદ કરી. + + %1$d વાર વપરાયેલ + %1$d વાર વપરાયેલ + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 22e3195..e1f5981 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -79,6 +79,24 @@ बहुत बढ़िया ध्यान देने की आवश्यकता है गंभीर - %1$d बार उपयोग किया गया कोई समस्या नहीं मिली। बहुत बढ़िया! + क्रम बदलें + संपादित करें + हटाएँ + विवरण जोड़ें + प्रविष्टि जोड़ें + सेटिंग्स + पासवर्ड बनाएँ + पासवर्ड विकल्प + खोज साफ़ करें + खोलें + वापस + पासवर्ड सेटिंग्स + लंबाई: %1$d + %1$d%% + अमान्य फ़ाइल चुनी गई। + + %1$d बार इस्तेमाल हुआ + %1$d बार इस्तेमाल हुआ + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index d80980e..f80bc96 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -79,6 +79,25 @@ Eccellente Richiede Attenzione Critico - Usata %1$d volte Nessun problema trovato. Ottimo lavoro! + Riordina + Modifica + Elimina + Aggiungi dettaglio + Aggiungi voce + Impostazioni + Genera password + Opzioni password + Cancella ricerca + Apri + Indietro + Impostazioni password + Lunghezza: %1$d + %1$d%% + File selezionato non valido. + + Usata %1$d volta + Usata %1$d volte + Usata %1$d volte + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 668931f..994c02e 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -79,6 +79,23 @@ 優秀 注意が必要 危険 - %1$d 回使用 問題は見つかりませんでした。素晴らしい! + 並べ替え + 編集 + 削除 + 詳細を追加 + 項目を追加 + 設定 + パスワードを生成 + パスワードのオプション + 検索をクリア + 開く + 戻る + パスワード設定 + 長さ: %1$d + %1$d%% + 選択したファイルは無効です。 + + %1$d 回使用 + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 80f19c6..e05c9ad 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -79,6 +79,23 @@ 훌륭함 주의 필요 심각함 - %1$d회 사용됨 문제가 발견되지 않았습니다. 잘하셨습니다! + 순서 변경 + 편집 + 삭제 + 세부 정보 추가 + 항목 추가 + 설정 + 비밀번호 생성 + 비밀번호 옵션 + 검색 지우기 + 열기 + 뒤로 + 비밀번호 설정 + 길이: %1$d + %1$d%% + 잘못된 파일을 선택했습니다. + + %1$d회 사용됨 + \ No newline at end of file diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 657cd85..3a2d924 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -79,6 +79,24 @@ उत्कृष्ट लक्ष देणे आवश्यक आहे गंभीर - %1$d वेळा वापरले कोणतीही समस्या आढळली नाही. छान! + क्रम बदला + संपादित करा + हटवा + तपशील जोडा + नोंद जोडा + सेटिंग्ज + पासवर्ड तयार करा + पासवर्ड पर्याय + शोध साफ करा + उघडा + मागे + पासवर्ड सेटिंग्ज + लांबी: %1$d + %1$d%% + अवैध फाइल निवडली. + + %1$d वेळा वापरले + %1$d वेळा वापरले + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..d05ab98 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #1C1B1F + diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..cea49f0 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index f39b7b3..ff05a74 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -79,6 +79,25 @@ Excelente Requer Atenção Crítico - Usado %1$d vezes Nenhum problema encontrado. Bom trabalho! + Reordenar + Editar + Excluir + Adicionar detalhe + Adicionar item + Configurações + Gerar senha + Opções de senha + Limpar busca + Abrir + Voltar + Configurações de senha + Comprimento: %1$d + %1$d%% + Arquivo selecionado inválido. + + Usada %1$d vez + Usada %1$d vezes + Usada %1$d vezes + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a2e5db9..3e4ae23 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -79,6 +79,26 @@ Отлично Требует внимания Критично - Использовано %1$d раз Проблем не обнаружено. Отличная работа! + Изменить порядок + Изменить + Удалить + Добавить деталь + Добавить запись + Настройки + Создать пароль + Параметры пароля + Очистить поиск + Открыть + Назад + Настройки пароля + Длина: %1$d + %1$d %% + Выбран недопустимый файл. + + Использован %1$d раз + Использован %1$d раза + Использован %1$d раз + Использован %1$d раза + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index aafd990..a82c42d 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -79,6 +79,24 @@ சிறந்தது கவனம் தேவை மிக முக்கியம் - %1$d முறை பயன்படுத்தப்பட்டது எந்த பிரச்சனையும் இல்லை. நன்று! + மறுவரிசைப்படுத்து + திருத்து + நீக்கு + விவரம் சேர் + உள்ளீடு சேர் + அமைப்புகள் + கடவுச்சொல் உருவாக்கு + கடவுச்சொல் விருப்பங்கள் + தேடலை அழி + திற + பின்செல் + கடவுச்சொல் அமைப்புகள் + நீளம்: %1$d + %1$d%% + தவறான கோப்பு தேர்ந்தெடுக்கப்பட்டது. + + %1$d முறை பயன்படுத்தப்பட்டது + %1$d முறை பயன்படுத்தப்பட்டது + diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index ea7126f..d3c1c77 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -79,7 +79,25 @@ అద్భుతం శ్రద్ధ అవసరం క్లిష్టమైన - %1$d సార్లు ఉపయోగించబడింది ఎటువంటి సమస్యలు కనుగొనబడలేదు. బాగా చేసారు! + క్రమం మార్చు + సవరించు + తొలగించు + వివరం జోడించు + ఎంట్రీ జోడించు + సెట్టింగ్‌లు + పాస్‌వర్డ్ సృష్టించు + పాస్‌వర్డ్ ఎంపికలు + శోధనను తొలగించు + తెరువు + వెనుకకు + పాస్‌వర్డ్ సెట్టింగ్‌లు + పొడవు: %1$d + %1$d%% + చెల్లని ఫైల్ ఎంపిక చేయబడింది. + + %1$d సారి ఉపయోగించబడింది + %1$d సార్లు ఉపయోగించబడింది + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d6594a0..41a38a1 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -79,6 +79,23 @@ 优秀 需要注意 严重 - 已使用 %1$d 次 未发现问题。干得好! + 重新排序 + 编辑 + 删除 + 添加详情 + 添加条目 + 设置 + 生成密码 + 密码选项 + 清除搜索 + 打开 + 返回 + 密码设置 + 长度:%1$d + %1$d%% + 选择的文件无效。 + + 已使用 %1$d 次 + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 3a09068..1187391 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,2 +1,9 @@ - \ No newline at end of file + + + #FFFBFE + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9da5e95..c68b6ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -100,7 +100,33 @@ Excellent Needs Attention Critical - Used %1$d times No issues found. Good job! + + + Reorder + Edit + Delete + Add detail + Add entry + Settings + Generate password + Password options + Clear search + Open + Back + Password settings + Length: %1$d + %1$d%% + Invalid file selected. + + + + Used %1$d time + Used %1$d times + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index c43cdf5..c412d4d 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -4,6 +4,12 @@ - \ No newline at end of file + From 0519798ed108ba0f7cb7e5c6dde1bbb27fa51b7c Mon Sep 17 00:00:00 2001 From: Aditya Bhardwaj Date: Wed, 16 Sep 2026 08:32:42 +0530 Subject: [PATCH 4/9] Secure sheets and dialogs, keep secrets off disk, add DB transactions Three pieces of security hygiene plus the repository support Phase 3 needs. FLAG_SECURE was set on the Activity window only. Compose Dialog and ModalBottomSheet are separate windows: under the currently resolved Compose versions their securePolicy defaults to SecureFlagPolicy.Inherit, so they do inherit it today and this was a latent risk rather than an active leak - but it breaks silently if a default changes or a sheet is ever hosted off a non-secure window, and the password-analysis sheet renders weak and reused passwords. All five ModalBottomSheets and all three dialogs now pin SecureFlagPolicy.SecureOn explicitly rather than relying on inheritance. DetailViewModel kept the in-progress entry title and answer in SavedStateHandle, including freshly generated passwords. SavedStateHandle is serialized into the saved-instance-state bundle, which the system persists to disk under /data/system_ce// - so cleartext secrets were being written outside the encrypted database. Both move to plain in-memory MutableStateFlows. The trade is that a half-typed entry is lost to process death, which is the right side of that trade for a password manager. The Add/Edit flag stays in SavedStateHandle, being a boolean rather than vault content. Added PasskeyRepository.runInTransaction and deleteAll, backed by Room's withTransaction, and used them immediately to fix reordering: both Preview and Detail reorder wrote N sequence updates in a bare loop, so an interruption left the list partially renumbered. Phase 3's import needs the same primitive to stop a malformed row leaving a half-populated vault. Co-Authored-By: Claude Opus 5 (1M context) --- .../passkey/data/local/dao/DetailsDao.kt | 4 ++ .../passkey/data/local/dao/PreviewDao.kt | 3 ++ .../data/repository/PassKeyRepository.kt | 14 ++++++ .../java/com/bhardwaj/passkey/di/AppModule.kt | 2 +- .../repository/PasskeyRepositoryImpl.kt | 11 +++++ .../domain/viewModels/DetailViewModel.kt | 48 +++++++++++-------- .../domain/viewModels/PreviewViewModel.kt | 14 ++++-- .../screens/detail_screen/DetailPage.kt | 3 ++ .../components/DetailsBottomSheet.kt | 5 ++ .../components/PasswordSettingsSheet.kt | 5 ++ .../screens/preview_screen/PreviewPage.kt | 3 ++ .../components/PreviewBottomSheet.kt | 5 ++ .../screens/security_screen/SecurityPage.kt | 2 + .../screens/settings_screen/SettingsPage.kt | 5 ++ .../components/AnalysisBottomSheet.kt | 5 ++ .../com/bhardwaj/passkey/utils/Constants.kt | 2 - 16 files changed, 103 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/bhardwaj/passkey/data/local/dao/DetailsDao.kt b/app/src/main/java/com/bhardwaj/passkey/data/local/dao/DetailsDao.kt index 4afb3a4..4e22df6 100644 --- a/app/src/main/java/com/bhardwaj/passkey/data/local/dao/DetailsDao.kt +++ b/app/src/main/java/com/bhardwaj/passkey/data/local/dao/DetailsDao.kt @@ -33,4 +33,8 @@ interface DetailsDao { @Query("UPDATE $DETAILS_TABLE SET sequence=:sequence WHERE detailsId=:detailsId") suspend fun updateDetailSequence(detailsId: Long, sequence: Long) + + @Query("DELETE FROM $DETAILS_TABLE") + suspend fun deleteAllDetails() + } \ No newline at end of file diff --git a/app/src/main/java/com/bhardwaj/passkey/data/local/dao/PreviewDao.kt b/app/src/main/java/com/bhardwaj/passkey/data/local/dao/PreviewDao.kt index d13f0f7..341364c 100644 --- a/app/src/main/java/com/bhardwaj/passkey/data/local/dao/PreviewDao.kt +++ b/app/src/main/java/com/bhardwaj/passkey/data/local/dao/PreviewDao.kt @@ -27,4 +27,7 @@ interface PreviewDao { @Query("UPDATE $PREVIEW_TABLE SET sequence=:sequence WHERE previewId=:previewId") suspend fun updatePreviewSequence(previewId: Long, sequence: Long) + + @Query("DELETE FROM $PREVIEW_TABLE") + suspend fun deleteAllPreviews() } \ No newline at end of file diff --git a/app/src/main/java/com/bhardwaj/passkey/data/repository/PassKeyRepository.kt b/app/src/main/java/com/bhardwaj/passkey/data/repository/PassKeyRepository.kt index 5b77af6..8be27f4 100644 --- a/app/src/main/java/com/bhardwaj/passkey/data/repository/PassKeyRepository.kt +++ b/app/src/main/java/com/bhardwaj/passkey/data/repository/PassKeyRepository.kt @@ -34,4 +34,18 @@ interface PasskeyRepository { suspend fun deletePreview(previews: Preview) suspend fun updatePreviewSequence(previewId: Long, sequence: Long) + + // Cross-cutting + + /** + * Runs [block] inside a single database transaction. + * + * Import previously wrote row by row with no transaction, so a malformed row part-way + * through a file left the vault half-populated with no way back. Reordering had the same + * problem: N separate sequence writes that could be interrupted between any two. + */ + suspend fun runInTransaction(block: suspend () -> R): R + + /** Removes every entry. Only meaningful inside [runInTransaction]. */ + suspend fun deleteAll() } \ No newline at end of file diff --git a/app/src/main/java/com/bhardwaj/passkey/di/AppModule.kt b/app/src/main/java/com/bhardwaj/passkey/di/AppModule.kt index 0cd61de..0fbb8b0 100644 --- a/app/src/main/java/com/bhardwaj/passkey/di/AppModule.kt +++ b/app/src/main/java/com/bhardwaj/passkey/di/AppModule.kt @@ -44,7 +44,7 @@ object AppModule { @Provides @Singleton fun provideTodoRepository(db: PassKeyDatabase): PasskeyRepository { - return PasskeyRepositoryImpl(db.previewDao, db.detailsDao) + return PasskeyRepositoryImpl(db, db.previewDao, db.detailsDao) } @Provides diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/repository/PasskeyRepositoryImpl.kt b/app/src/main/java/com/bhardwaj/passkey/domain/repository/PasskeyRepositoryImpl.kt index 343bd78..42ebe27 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/repository/PasskeyRepositoryImpl.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/repository/PasskeyRepositoryImpl.kt @@ -1,5 +1,7 @@ package com.bhardwaj.passkey.domain.repository +import androidx.room.withTransaction +import com.bhardwaj.passkey.data.local.PassKeyDatabase import com.bhardwaj.passkey.data.local.dao.DetailsDao import com.bhardwaj.passkey.data.local.dao.PreviewDao import com.bhardwaj.passkey.data.local.entity.Details @@ -8,9 +10,18 @@ import com.bhardwaj.passkey.data.repository.PasskeyRepository import kotlinx.coroutines.flow.Flow class PasskeyRepositoryImpl( + private val database: PassKeyDatabase, private val previewDao: PreviewDao, private val detailsDao: DetailsDao ) : PasskeyRepository { + + override suspend fun runInTransaction(block: suspend () -> R): R = + database.withTransaction { block() } + + override suspend fun deleteAll() { + detailsDao.deleteAllDetails() + previewDao.deleteAllPreviews() + } override fun getDetails(): Flow> { return detailsDao.getDetails() } diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt index 8ab2f38..a4939ab 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/DetailViewModel.kt @@ -12,8 +12,6 @@ import com.bhardwaj.passkey.data.local.entity.Details import com.bhardwaj.passkey.data.repository.PasskeyRepository import com.bhardwaj.passkey.domain.events.DetailEvents import com.bhardwaj.passkey.utils.Constants.Companion.BOTTOM_SHEET_HEADING -import com.bhardwaj.passkey.utils.Constants.Companion.DETAIL_RESPONSE -import com.bhardwaj.passkey.utils.Constants.Companion.DETAIL_TITLE import com.bhardwaj.passkey.utils.PasswordGenerator import com.bhardwaj.passkey.utils.UiEvents import com.bhardwaj.passkey.utils.UiText @@ -37,8 +35,16 @@ class DetailViewModel @Inject constructor( private val _uiEvents = Channel() val uiEvents = _uiEvents.receiveAsFlow() - val detailTitle = savedStateHandle.getStateFlow(DETAIL_TITLE, "") - val detailResponse = savedStateHandle.getStateFlow(DETAIL_RESPONSE, "") + // Deliberately NOT in SavedStateHandle. That is serialized into the saved-instance-state + // bundle, which the system writes to disk under /data/system_ce// for task + // persistence - so the in-progress secret, including a freshly generated password, would be + // stored in cleartext outside the encrypted database. Losing a half-typed entry to process + // death is the correct trade here. + private val _detailTitle = MutableStateFlow("") + val detailTitle = _detailTitle.asStateFlow() + + private val _detailResponse = MutableStateFlow("") + val detailResponse = _detailResponse.asStateFlow() /** true = editing an existing row, false = adding. Resolved to text by the UI. */ val isEditingSheet = savedStateHandle.getStateFlow(BOTTOM_SHEET_HEADING, false) val previewId = savedStateHandle.get("previewId") ?: -1 @@ -95,8 +101,8 @@ class DetailViewModel @Inject constructor( } DetailEvents.OnDismissBottomSheet -> { - savedStateHandle[DETAIL_TITLE] = "" - savedStateHandle[DETAIL_RESPONSE] = "" + _detailTitle.value = "" + _detailResponse.value = "" isSheetOpen = false } @@ -106,27 +112,27 @@ class DetailViewModel @Inject constructor( repository.getDetailById(event.details.detailsId!!)?.let { detail -> isSheetOpen = true savedStateHandle[BOTTOM_SHEET_HEADING] = true - savedStateHandle[DETAIL_TITLE] = event.details.question - savedStateHandle[DETAIL_RESPONSE] = event.details.answer + _detailTitle.value = event.details.question + _detailResponse.value = event.details.answer this@DetailViewModel.detail = detail } } } is DetailEvents.OnTitleChange -> { - savedStateHandle[DETAIL_TITLE] = event.newTitle + _detailTitle.value = event.newTitle } is DetailEvents.OnDescriptionChange -> { - savedStateHandle[DETAIL_RESPONSE] = event.newDescription + _detailResponse.value = event.newDescription } DetailEvents.OnSaveClick -> { viewModelScope.launch { if (detailTitle.value.isBlank() or detailResponse.value.isBlank()) { isSheetOpen = false - savedStateHandle[DETAIL_TITLE] = "" - savedStateHandle[DETAIL_RESPONSE] = "" + _detailTitle.value = "" + _detailResponse.value = "" sendUiEvents( UiEvents.ShowSnackBar( message = UiText.StringResource(R.string.enter_valid_title_n_response) @@ -162,8 +168,8 @@ class DetailViewModel @Inject constructor( } ?: repository.upsertDetails(newDetail) detail = null - savedStateHandle[DETAIL_TITLE] = "" - savedStateHandle[DETAIL_RESPONSE] = "" + _detailTitle.value = "" + _detailResponse.value = "" isSheetOpen = false } } @@ -215,11 +221,13 @@ class DetailViewModel @Inject constructor( detail.copy(sequence = index.toLong()) } - updatedList.forEach { detail -> - repository.updateDetailSequence( - detailId = detail.detailsId!!, - sequence = detail.sequence - ) + repository.runInTransaction { + updatedList.forEach { detail -> + repository.updateDetailSequence( + detailId = detail.detailsId!!, + sequence = detail.sequence + ) + } } } } @@ -232,7 +240,7 @@ class DetailViewModel @Inject constructor( includeNumbers = includeNumbers, includeSpecial = includeSpecial ) - savedStateHandle[DETAIL_RESPONSE] = newPassword + _detailResponse.value = newPassword } DetailEvents.OnPasswordSettingsClick -> { diff --git a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt index d695c53..90a35bf 100644 --- a/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt +++ b/app/src/main/java/com/bhardwaj/passkey/domain/viewModels/PreviewViewModel.kt @@ -217,11 +217,15 @@ class PreviewViewModel @Inject constructor( preview.copy(sequence = index.toLong()) } - updatedList.forEach { preview -> - repository.updatePreviewSequence( - previewId = preview.previewId!!, - sequence = preview.sequence - ) + // One transaction, not N separate writes: a reorder interrupted midway + // used to leave the list in a partially renumbered state. + repository.runInTransaction { + updatedList.forEach { preview -> + repository.updatePreviewSequence( + previewId = preview.previewId!!, + sequence = preview.sequence + ) + } } } } diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt index 8e90792..5b2ea51 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/DetailPage.kt @@ -38,6 +38,8 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource @@ -295,6 +297,7 @@ fun DetailScreen( } if (viewModel.isAlertOpen) { AlertDialog( + properties = DialogProperties(securePolicy = SecureFlagPolicy.SecureOn), containerColor = MaterialTheme.colorScheme.background, onDismissRequest = { viewModel.onEvent(DetailEvents.OnDismissAlertDialog) diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsBottomSheet.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsBottomSheet.kt index 6f49db2..a17b5a3 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsBottomSheet.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/DetailsBottomSheet.kt @@ -22,6 +22,8 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -57,6 +59,9 @@ fun DetailsBottomSheet( ) { val sheetState = rememberModalBottomSheetState() ModalBottomSheet( + properties = ModalBottomSheetProperties( + securePolicy = SecureFlagPolicy.SecureOn + ), sheetState = sheetState, onDismissRequest = { onDismiss() }, dragHandle = {}, diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/PasswordSettingsSheet.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/PasswordSettingsSheet.kt index 339eeee..9fa3c4a 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/PasswordSettingsSheet.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/detail_screen/components/PasswordSettingsSheet.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Switch @@ -44,6 +46,9 @@ fun PasswordSettingsSheet( val sheetState = rememberModalBottomSheetState() ModalBottomSheet( + properties = ModalBottomSheetProperties( + securePolicy = SecureFlagPolicy.SecureOn + ), sheetState = sheetState, onDismissRequest = { onDismissPasswordSettings() }, dragHandle = { }, diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt index 11304b4..3f0ec41 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/PreviewPage.kt @@ -49,6 +49,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource @@ -318,6 +320,7 @@ fun PreviewScreen( } if (viewModel.isAlertOpen) { AlertDialog( + properties = DialogProperties(securePolicy = SecureFlagPolicy.SecureOn), containerColor = MaterialTheme.colorScheme.background, onDismissRequest = { viewModel.onEvent(PreviewEvents.OnDismissAlertDialog) diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomSheet.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomSheet.kt index 6bebbe1..afedf89 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomSheet.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/preview_screen/components/PreviewBottomSheet.kt @@ -10,6 +10,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -41,6 +43,9 @@ fun PreviewBottomSheet( ) { val sheetState = rememberModalBottomSheetState() ModalBottomSheet( + properties = ModalBottomSheetProperties( + securePolicy = SecureFlagPolicy.SecureOn + ), sheetState = sheetState, onDismissRequest = { onDismiss() }, dragHandle = {}, diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/security_screen/SecurityPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/security_screen/SecurityPage.kt index 314207e..fa49a77 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/security_screen/SecurityPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/security_screen/SecurityPage.kt @@ -45,6 +45,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.SecureFlagPolicy import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -126,6 +127,7 @@ fun SecurityScreen( if (viewModel.needToOpenAlertDialog) { Dialog( onDismissRequest = {}, properties = DialogProperties( + securePolicy = SecureFlagPolicy.SecureOn, dismissOnBackPress = false, dismissOnClickOutside = false ) diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/SettingsPage.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/SettingsPage.kt index 2335c78..72ff869 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/SettingsPage.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/SettingsPage.kt @@ -20,6 +20,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -122,6 +124,9 @@ fun SettingsScreen( if (viewModel.isSheetOpen) { val sheetState = rememberModalBottomSheetState() ModalBottomSheet( + properties = ModalBottomSheetProperties( + securePolicy = SecureFlagPolicy.SecureOn + ), sheetState = sheetState, onDismissRequest = { viewModel.onEvent(SettingsEvents.OnDismissBottomSheet) }, dragHandle = {}, diff --git a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/AnalysisBottomSheet.kt b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/AnalysisBottomSheet.kt index d736d32..ead603a 100644 --- a/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/AnalysisBottomSheet.kt +++ b/app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/AnalysisBottomSheet.kt @@ -29,6 +29,8 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.ui.window.SecureFlagPolicy +import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -64,6 +66,9 @@ fun AnalysisBottomSheet( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( + properties = ModalBottomSheetProperties( + securePolicy = SecureFlagPolicy.SecureOn + ), sheetState = sheetState, onDismissRequest = { onDismiss() }, containerColor = MaterialTheme.colorScheme.background diff --git a/app/src/main/java/com/bhardwaj/passkey/utils/Constants.kt b/app/src/main/java/com/bhardwaj/passkey/utils/Constants.kt index 5586d59..09dd5ec 100644 --- a/app/src/main/java/com/bhardwaj/passkey/utils/Constants.kt +++ b/app/src/main/java/com/bhardwaj/passkey/utils/Constants.kt @@ -15,8 +15,6 @@ class Constants { // SaveState Constants. const val PREVIEW_HEADING = "preview_heading" const val PREVIEW_CATEGORY_NAME = "preview_category_name" - const val DETAIL_TITLE = "detail_title" - const val DETAIL_RESPONSE = "detail_response" const val BOTTOM_SHEET_HEADING = "bottom_sheet_heading" // DataStore Prefs Constants. From 89f43e75084804171ae64d54916555db7b61dc79 Mon Sep 17 00:00:00 2001 From: Aditya Bhardwaj Date: Wed, 16 Sep 2026 08:45:55 +0530 Subject: [PATCH 5/9] Replace the plaintext CSV export with an encrypted backup format Export wrote the entire vault as readable CSV into the public Downloads folder. The .passkey extension and application/passkey MIME type were cosmetic: the bytes were credentials in cleartext, visible to any app with media access and swept up by cloud backup tools. This is the single worst data-handling defect in the app. New .pkbak format: "PKBACKUP" magic, format version, KDF id and parameters, salt, nonce and length, followed by AES-256-GCM ciphertext over GZIPped JSON. The whole 51-byte header is passed to GCM as additional authenticated data, so the KDF identifier and its parameters are covered by the auth tag - otherwise an attacker could rewrite kdfId to name a weaker KDF and the file would still verify. There is a test for exactly that. Keys come from PBKDF2-HMAC-SHA512 at 600k iterations. Argon2id is stronger against GPU attack and is what the kdfId field exists to allow; it needs a native dependency and is planned alongside the 5.7.0 key work, at which point new files get kdfId 2 and this reader keeps handling kdfId 1. Files are written through SAF CreateDocument so the user picks the destination, using "wt" so an existing file is truncated rather than partially overwritten. The backup password is collected only after a destination is chosen, and entered twice with an explicit warning that it cannot be recovered. Import is hardened throughout: - format detection is by magic bytes, not filename. The old check matched a display name ending in "passkey", which would have rejected every new backup and trusted a user-supplied name to decide how to parse the contents. - the whole restore runs in one transaction, so a bad row part-way through a file can no longer leave a half-populated vault. - unknown categories map to OTHERS instead of Categories.valueOf throwing into a broad catch that silently abandoned the rest of the file. - field lengths, row counts and file size are bounded against a hostile file. - a wrong password and a corrupt file are deliberately indistinguishable, since GCM cannot tell them apart and pretending otherwise leaks whether a guess was close. The legacy plaintext CSV stays readable so existing backups still restore, now via opencsv - which was already a declared but entirely unused dependency - so quoted values containing commas parse correctly rather than being split blindly. Rows with the wrong column count are dropped and counted rather than aborting the import. The lossy "____" comma sentinel is un-escaped on read; nothing can recover a value that genuinely contained it, but that is what the file means. Only the reader survives; the CSV writer is gone. SettingsViewModel drops from 340 to 212 lines with the file I/O and MediaStore code removed. Manifest, now that SAF has removed the need for storage access: - READ_/WRITE_EXTERNAL_STORAGE deleted. They were capped at maxSdkVersion 28 while minSdk is 28, so they were live on exactly the oldest supported devices. - allowBackup="false". Platform backup would carry the SQLCipher database off the device, and with today's build-time key that copy is decryptable by anyone with the APK. backup_rules.xml and data_extraction_rules.xml were untouched IDE templates and are now correct regardless, so flipping the flag back cannot silently start uploading the vault. - INTERNET is declared explicitly. The app makes no network calls itself, but Firebase and Play in-app-update require it, and having it merged in invisibly from a library manifest is how the README came to claim otherwise. The merged manifest of a built APK also shows AD_ID and the AdServices permissions arriving from firebase-analytics, which the privacy rewrite has to disclose. - MainActivity is singleTask so a second instance cannot come up holding a stale unlocked session once app-lock lands. 24 new unit tests cover the crypto round-trip, tamper detection on both ciphertext and header, and the legacy CSV reader including the shapes the old exporter got wrong. Round-tripping a value containing commas, newlines, the ____ sentinel and emoji is now covered, none of which survived the old format. Co-Authored-By: Claude Opus 5 (1M context) --- .idea/deploymentTargetSelector.xml | 1 + .idea/kotlinc.xml | 4 +- .idea/ktlint-plugin.xml | 7 + app/build.gradle.kts | 6 +- app/src/main/AndroidManifest.xml | 20 +- .../passkey/data/backup/BackupCrypto.kt | 128 +++++++++ .../passkey/data/backup/BackupFormat.kt | 126 +++++++++ .../passkey/data/backup/BackupPayload.kt | 42 +++ .../passkey/data/backup/BackupRepository.kt | 242 ++++++++++++++++++ .../passkey/data/backup/LegacyCsvBackup.kt | 68 +++++ .../passkey/domain/events/SettingsEvents.kt | 30 ++- .../domain/viewModels/SettingsViewModel.kt | 192 +++----------- .../screens/settings_screen/SettingsPage.kt | 108 +++++--- .../components/BackupPasswordDialog.kt | 123 +++++++++ .../com/bhardwaj/passkey/utils/Constants.kt | 4 - app/src/main/res/values-ar/strings.xml | 16 +- app/src/main/res/values-bn/strings.xml | 16 +- app/src/main/res/values-de/strings.xml | 16 +- app/src/main/res/values-es/strings.xml | 16 +- app/src/main/res/values-fr/strings.xml | 16 +- app/src/main/res/values-gu/strings.xml | 16 +- app/src/main/res/values-hi/strings.xml | 16 +- app/src/main/res/values-it/strings.xml | 16 +- app/src/main/res/values-ja/strings.xml | 16 +- app/src/main/res/values-ko/strings.xml | 16 +- app/src/main/res/values-mr/strings.xml | 16 +- app/src/main/res/values-pt/strings.xml | 16 +- app/src/main/res/values-ru/strings.xml | 16 +- app/src/main/res/values-ta/strings.xml | 16 +- app/src/main/res/values-te/strings.xml | 16 +- app/src/main/res/values-zh/strings.xml | 16 +- app/src/main/res/values/strings.xml | 18 +- app/src/main/res/xml/backup_rules.xml | 23 +- .../main/res/xml/data_extraction_rules.xml | 26 +- .../com/bhardwaj/passkey/BackupCryptoTest.kt | 98 +++++++ .../bhardwaj/passkey/LegacyCsvBackupTest.kt | 78 ++++++ gradle/libs.versions.toml | 3 + 37 files changed, 1355 insertions(+), 248 deletions(-) create mode 100644 .idea/ktlint-plugin.xml create mode 100644 app/src/main/java/com/bhardwaj/passkey/data/backup/BackupCrypto.kt create mode 100644 app/src/main/java/com/bhardwaj/passkey/data/backup/BackupFormat.kt create mode 100644 app/src/main/java/com/bhardwaj/passkey/data/backup/BackupPayload.kt create mode 100644 app/src/main/java/com/bhardwaj/passkey/data/backup/BackupRepository.kt create mode 100644 app/src/main/java/com/bhardwaj/passkey/data/backup/LegacyCsvBackup.kt create mode 100644 app/src/main/java/com/bhardwaj/passkey/presentation/screens/settings_screen/components/BackupPasswordDialog.kt create mode 100644 app/src/test/java/com/bhardwaj/passkey/BackupCryptoTest.kt create mode 100644 app/src/test/java/com/bhardwaj/passkey/LegacyCsvBackupTest.kt diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..ca16a99 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,7 @@ diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index aabf371..1dd6491 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -4,8 +4,8 @@

): PasswordAnalysisResult { - val passwordEntries = details.filter { - val q = it.question.lowercase() - q.contains("password") || q.contains("pin") || q.contains("code") || q.contains("secret") - } - - val weakList = mutableListOf
() - val reuseMap = mutableMapOf>() - - passwordEntries.forEach { detail -> - val pass = detail.answer - - if (isWeak(pass)) { - weakList.add(detail) - } + /** + * Analyses the vault. + * + * [secretKeywords] must be supplied by the caller from `R.array.secret_field_keywords`, + * unioned across the default and current locale. Previously this object hardcoded the + * English words "password", "pin", "code" and "secret", so across the app's 16 non-English + * locales nothing matched: totalPasswords came back 0, the score came back 100, and users + * were told their vault was perfect. That is worse than having no feature at all. + */ + fun analyze(details: List
, secretKeywords: Set): PasswordAnalysisResult { + val normalizedKeywords = secretKeywords.map { it.normalizeForMatch() }.filter { it.isNotBlank() } - if (reuseMap.containsKey(pass)) { - reuseMap[pass]?.add(detail) - } else { - reuseMap[pass] = mutableListOf(detail) + val passwordEntries = details.filter { detail -> + detail.isSecret || normalizedKeywords.any { keyword -> + detail.question.normalizeForMatch().contains(keyword) } } - val actualReused = reuseMap.filter { it.value.size > 1 } + val weakList = passwordEntries.filter { isWeak(it.answer) } - var score = 100 - val weakPenalty = (weakList.size * 10).coerceAtMost(50) - val reusedPenalty = (actualReused.size * 15).coerceAtMost(50) + // Keyed by a digest rather than the plaintext: this map is held in ViewModel state and + // rendered by the analysis sheet, and there is no reason for cleartext passwords to sit + // in a composition-scoped object. + val reused = passwordEntries + .groupBy { it.answer } + .filterValues { it.size > 1 } + .mapKeys { (password, _) -> password.digestKey() } - score -= (weakPenalty + reusedPenalty) - if (passwordEntries.isEmpty()) score = 100 + var score = 100 + if (passwordEntries.isNotEmpty()) { + val weakPenalty = (weakList.size * 10).coerceAtMost(50) + // Counts affected entries, not distinct duplicated values: three copies of one + // password used to score the same as two, which understated the problem. + val reusedEntryCount = reused.values.sumOf { it.size } + val reusedPenalty = (reusedEntryCount * 15).coerceAtMost(50) + score -= (weakPenalty + reusedPenalty) + } return PasswordAnalysisResult( totalPasswords = passwordEntries.size, weakPasswords = weakList, - reusedPasswords = actualReused, + reusedPasswords = reused, strengthScore = score.coerceAtLeast(0) ) } @@ -55,6 +63,20 @@ object PasswordAnalyzer { if (password.length < 8) return true if (password.all { it.isDigit() }) return true if (password.all { it.isLetter() }) return true + if (password.toSet().size == 1) return true return false } -} \ No newline at end of file + + /** Case-folded and accent-stripped, so "Contraseña" and "contrasena" both match. */ + private fun String.normalizeForMatch(): String = + Normalizer.normalize(this, Normalizer.Form.NFKD) + .replace(Regex("\\p{Mn}+"), "") + .lowercase(Locale.ROOT) + .trim() + + private fun String.digestKey(): String = + java.security.MessageDigest.getInstance("SHA-256") + .digest(toByteArray(Charsets.UTF_8)) + .take(8) + .joinToString("") { "%02x".format(it) } +} diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a16f75c..5fa2e9c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -130,4 +130,13 @@ بعد 5 دقائق أبدًا ستبقى الخزنة مفتوحة حتى تغلق التطبيق. يمكن لأي شخص يحمل هاتفك المفتوح قراءة كلمات مرورك. + + كلمة المرور + كلمة السر + رمز + الرقم السري + سر + مفتاح + تسجيل الدخول + \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index a52464b..f8a9cfc 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -126,4 +126,13 @@ ৫ মিনিট পরে কখনও নয় অ্যাপ বন্ধ না করা পর্যন্ত ভল্ট আনলক থাকবে। আপনার আনলক করা ফোন যার হাতে থাকবে সে পাসওয়ার্ড দেখতে পারবে। + + পাসওয়ার্ড + পিন + কোড + গোপন + গুপ্ত + চাবি + লগইন + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b7345e3..117f6e4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -126,4 +126,14 @@ Nach 5 Minuten Nie Der Tresor bleibt offen, bis du die App schließt. Wer dein entsperrtes Telefon hat, kann deine Passwörter lesen. + + passwort + kennwort + pin + code + geheim + schlüssel + zugangsdaten + anmeldung + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ac4ab10..2547279 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -127,4 +127,13 @@ Tras 5 minutos Nunca La caja fuerte seguirá abierta hasta que cierres la app. Quien tenga tu teléfono desbloqueado podrá leer tus contraseñas. + + contraseña + clave + pin + código + secreto + llave + acceso + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6e2b961..c819cbf 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -127,4 +127,13 @@ Après 5 minutes Jamais Le coffre restera déverrouillé jusqu\'à la fermeture de l\'application. Toute personne ayant votre téléphone déverrouillé pourra lire vos mots de passe. + + mot de passe + code + pin + secret + clé + identifiant + connexion + diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index 21a5e36..b7e07d6 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -126,4 +126,12 @@ 5 મિનિટ પછી ક્યારેય નહીં ઍપ બંધ ન કરો ત્યાં સુધી વૉલ્ટ ખુલ્લી રહેશે. તમારો અનલૉક કરેલો ફોન જેની પાસે હશે તે તમારા પાસવર્ડ વાંચી શકશે. + + પાસવર્ડ + પિન + કોડ + ગુપ્ત + ચાવી + લૉગિન + diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 2765c37..1c90f86 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -126,4 +126,13 @@ 5 मिनट बाद कभी नहीं ऐप बंद करने तक वॉल्ट खुली रहेगी। जिसके पास आपका अनलॉक फ़ोन होगा वह आपके पासवर्ड पढ़ सकता है। + + पासवर्ड + पिन + कोड + गुप्त + गोपनीय + कुंजी + लॉगिन + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 4d322ef..b92df5a 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -127,4 +127,13 @@ Dopo 5 minuti Mai La cassaforte resterà aperta finché non chiudi l\'app. Chi ha il tuo telefono sbloccato potrà leggere le tue password. + + password + parola d\'ordine + pin + codice + segreto + chiave + accesso + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a61cb71..9ee1dd5 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -125,4 +125,12 @@ 5 分後 しない アプリを閉じるまで保管庫は開いたままになります。ロック解除された端末を手にした人は誰でもパスワードを見られます。 + + パスワード + 暗証番号 + コード + 秘密 + 鍵 + ログイン + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 6eb4952..9931faf 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -125,4 +125,13 @@ 5분 후 안 함 앱을 닫을 때까지 보관함이 열린 채로 유지됩니다. 잠금 해제된 기기를 가진 사람은 누구나 비밀번호를 볼 수 있습니다. + + 비밀번호 + 암호 + 핀 + 코드 + 비밀 + 키 + 로그인 + \ No newline at end of file diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index d2bc662..5366c0d 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -126,4 +126,13 @@ 5 मिनिटांनी कधीही नाही ॲप बंद करेपर्यंत व्हॉल्ट उघडी राहील. तुमचा अनलॉक केलेला फोन ज्याच्याकडे असेल तो तुमचे पासवर्ड वाचू शकतो. + + पासवर्ड + पिन + कोड + गुप्त + गोपनीय + किल्ली + लॉगिन + \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index fd785f0..1fcca50 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -127,4 +127,13 @@ Após 5 minutos Nunca O cofre ficará aberto até você fechar o app. Quem tiver seu celular desbloqueado poderá ler suas senhas. + + senha + palavra-passe + pin + código + segredo + chave + acesso + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a71238a..2672789 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -128,4 +128,12 @@ Через 5 минут Никогда Хранилище останется разблокированным, пока вы не закроете приложение. Любой, у кого окажется разблокированный телефон, увидит ваши пароли. + + пароль + пин + код + секрет + ключ + вход + diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 4c6c838..c9d0355 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -126,4 +126,12 @@ 5 நிமிடங்களுக்குப் பிறகு ஒருபோதும் இல்லை ஆப்ஸை மூடும் வரை பெட்டகம் திறந்தே இருக்கும். உங்கள் திறந்த ஃபோன் யாரிடம் இருந்தாலும் கடவுச்சொற்களைப் படிக்க முடியும். + + கடவுச்சொல் + பின் + குறியீடு + ரகசியம் + சாவி + உள்நுழைவு + diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index d925d26..c1ca3fa 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -126,5 +126,13 @@ 5 నిమిషాల తర్వాత ఎప్పుడూ కాదు యాప్ మూసే వరకు వాల్ట్ తెరిచే ఉంటుంది. మీ అన్‌లాక్ చేసిన ఫోన్ ఎవరి దగ్గర ఉన్నా మీ పాస్‌వర్డ్‌లు చదవగలరు. + + పాస్‌వర్డ్ + పిన్ + కోడ్ + రహస్యం + కీ + లాగిన్ + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 0d8090a..c8e88d2 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -125,4 +125,13 @@ 5 分钟后 永不 在你关闭应用之前密码库将保持解锁。任何拿到你已解锁手机的人都能读取你的密码。 + + 密码 + 口令 + 密钥 + 验证码 + 代码 + 秘密 + 登录 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a53f86..c283603 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -162,4 +162,21 @@ After 5 minutes Never The vault will stay unlocked until you close the app. Anyone with your unlocked phone can read your passwords. + + + + password + pass + pin + passcode + secret + code + key + login + credential + \ No newline at end of file diff --git a/app/src/test/java/com/bhardwaj/passkey/PasswordAnalyzerTest.kt b/app/src/test/java/com/bhardwaj/passkey/PasswordAnalyzerTest.kt new file mode 100644 index 0000000..bdc68a8 --- /dev/null +++ b/app/src/test/java/com/bhardwaj/passkey/PasswordAnalyzerTest.kt @@ -0,0 +1,96 @@ +package com.bhardwaj.passkey + +import com.bhardwaj.passkey.data.local.entity.Details +import com.bhardwaj.passkey.utils.PasswordAnalyzer +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class PasswordAnalyzerTest { + + private val english = setOf("password", "pin", "code", "secret") + private val hindi = setOf("पासवर्ड", "पिन") + + private fun detail( + id: Long, question: String, answer: String, isSecret: Boolean = false + ) = Details( + detailsId = id, previewId = 1, question = question, answer = answer, isSecret = isSecret + ) + + @Test + fun `empty vault scores 100`() { + val result = PasswordAnalyzer.analyze(emptyList(), english) + assertThat(result.strengthScore).isEqualTo(100) + assertThat(result.totalPasswords).isEqualTo(0) + } + + @Test + fun `entries labelled in a non-English locale are classified`() { + // The whole point: with hardcoded English keywords this returned 0 passwords and a + // perfect score for every user of the 16 translated locales. + val details = listOf(detail(1, "पासवर्ड", "abc")) + val result = PasswordAnalyzer.analyze(details, english + hindi) + assertThat(result.totalPasswords).isEqualTo(1) + assertThat(result.weakPasswords).hasSize(1) + } + + @Test + fun `the isSecret flag classifies regardless of label language`() { + val details = listOf(detail(1, "مفتاح", "abc", isSecret = true)) + val result = PasswordAnalyzer.analyze(details, english) + assertThat(result.totalPasswords).isEqualTo(1) + } + + @Test + fun `matching ignores case and accents`() { + val details = listOf(detail(1, "Contraseña principal", "abc")) + val result = PasswordAnalyzer.analyze(details, setOf("contrasena")) + assertThat(result.totalPasswords).isEqualTo(1) + } + + @Test + fun `non-secret fields are ignored`() { + val details = listOf(detail(1, "Username", "alice"), detail(2, "Notes", "hello")) + assertThat(PasswordAnalyzer.analyze(details, english).totalPasswords).isEqualTo(0) + } + + @Test + fun `weak passwords are detected`() { + val details = listOf( + detail(1, "Password", "short"), // < 8 + detail(2, "PIN", "12345678"), // all digits + detail(3, "Password 3", "abcdefgh"), // all letters + detail(4, "Password 4", "aaaaaaaa"), // single repeated character + detail(5, "Password 5", "Str0ng!Pass") // fine + ) + val result = PasswordAnalyzer.analyze(details, english) + assertThat(result.weakPasswords.map { it.detailsId }).containsExactly(1L, 2L, 3L, 4L) + } + + @Test + fun `weak penalty is capped at 50`() { + val details = (1..10L).map { detail(it, "Password $it", "abc") } + // Ten weak entries would be -100 uncapped; the floor must still be 50 here. + val result = PasswordAnalyzer.analyze(details, english) + assertThat(result.strengthScore).isAtLeast(0) + assertThat(result.weakPasswords).hasSize(10) + } + + @Test + fun `reuse groups only include duplicates and are not keyed by plaintext`() { + val details = listOf( + detail(1, "Password A", "Repeated1!"), + detail(2, "Password B", "Repeated1!"), + detail(3, "Password C", "Unique9!x") + ) + val result = PasswordAnalyzer.analyze(details, english) + assertThat(result.reusedPasswords).hasSize(1) + assertThat(result.reusedPasswords.values.single()).hasSize(2) + assertThat(result.reusedPasswords.keys.single()).doesNotContain("Repeated1!") + } + + @Test + fun `score never drops below zero`() { + val details = (1..40L).map { detail(it, "Password $it", "aaa") } + assertThat(PasswordAnalyzer.analyze(details, english).strengthScore).isAtLeast(0) + } +} From f5eb611849516a543f5c0e27d0d08a59dc442b1d Mon Sep 17 00:00:00 2001 From: Aditya Bhardwaj Date: Wed, 16 Sep 2026 09:28:59 +0530 Subject: [PATCH 9/9] Correct the privacy claims, and translate About/Privacy/Terms into all locales The in-app About, Privacy and Terms text, README.md and Privacy Policy.md all stated that the app requires no internet permission and shares nothing with third parties. That was false in the shipped APK. Dumping the merged manifest of a build shows INTERNET, ACCESS_NETWORK_STATE, WAKE_LOCK, AD_ID and the Play AdServices permissions all arriving from firebase-analytics and the Play update library. The repo's Privacy Policy.md simultaneously contradicted the in-app copy by describing IP address and device name collection. All three in-app texts are rewritten to be accurate and, deliberately, far shorter. The previous About text was several hundred words of marketing that also claimed LiveData and MVVM (the app uses Flow), dark mode (half-implemented until this release) and Autofill (still not implemented). Accurate, concise copy is both better for users and tractable to translate. The rewritten text states plainly: vault entries are never collected, transmitted or stored by us and cannot be recovered by us; Firebase Crashlytics, Analytics and Performance Monitoring plus Play in-app updates send crash diagnostics, usage events, performance traces and device/installation identifiers including an advertising ID to Google, and never have access to vault contents; backups are the user's responsibility once written; and the vault is excluded from Android cloud backup and device transfer. Privacy Policy.md is rewritten around the actual architecture with a per-component table of what each one sends and a permission-by-permission justification. README.md drops "Fully Offline : No internet permission required", gains a security model section, and states the current database-key limitation openly rather than letting the feature list imply protection the build does not yet provide. With the text corrected it is now safe to translate, which completes the decision to localize everything: about_message, privacy_message and terms_n_condition_message are translated into all 16 locales. app_name is the only remaining translatable="false" string, being the brand name. These three are long-form legal and descriptive text produced by machine translation. They should get a native-speaker review pass before release; the technical claims in them are accurate, but phrasing and legal register have not been checked by a human for each language. Version bumped to 5.6.0 (versionCode 45). Co-Authored-By: Claude Opus 5 (1M context) --- Privacy Policy.md | 152 +++++++++++-------------- README.md | 53 ++++++--- app/build.gradle.kts | 4 +- app/src/main/res/values-ar/strings.xml | 3 + app/src/main/res/values-bn/strings.xml | 3 + app/src/main/res/values-de/strings.xml | 3 + app/src/main/res/values-es/strings.xml | 3 + app/src/main/res/values-fr/strings.xml | 3 + app/src/main/res/values-gu/strings.xml | 3 + app/src/main/res/values-hi/strings.xml | 3 + app/src/main/res/values-it/strings.xml | 3 + app/src/main/res/values-ja/strings.xml | 3 + app/src/main/res/values-ko/strings.xml | 3 + app/src/main/res/values-mr/strings.xml | 3 + app/src/main/res/values-pt/strings.xml | 3 + app/src/main/res/values-ru/strings.xml | 3 + app/src/main/res/values-ta/strings.xml | 3 + app/src/main/res/values-te/strings.xml | 3 + app/src/main/res/values-zh/strings.xml | 3 + app/src/main/res/values/strings.xml | 49 ++++++-- 20 files changed, 194 insertions(+), 112 deletions(-) diff --git a/Privacy Policy.md b/Privacy Policy.md index d2b196d..3466546 100644 --- a/Privacy Policy.md +++ b/Privacy Policy.md @@ -1,84 +1,68 @@ -# Privacy Policy - -Aditya Bhardwaj built the PassKey app as an Open Source app. This SERVICE is provided by Aditya -Bhardwaj at no cost and is intended for use as is. -This page is used to inform visitors regarding my policies with the collection, use, and disclosure -of Personal Information if anyone decided to use my Service. -If you choose to use my Service, then you agree to the collection and use of information in relation -to this policy. The Personal Information that I collect is used for providing and improving the -Service. I will not use or share your information with anyone except as described in this Privacy -Policy. -The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which -are accessible at PassKey unless otherwise defined in this Privacy Policy. - -# Information Collection and Use - -For a better experience, while using our Service, I may require you to provide us with certain -personally identifiable information, including but not limited to Aditya Bhardwaj. The information -that I request will be retained on your device and is not collected by me in any way. -The app does use third-party services that may collect information used to identify you. -Link to the privacy policy of third-party service providers used by the app -Google Play Services - -# Log Data - -I want to inform you that whenever you use my Service, in a case of an error in the app I collect -data and information (through third-party products) on your phone called Log Data. This Log Data may -include information such as your device Internet Protocol (“IP”) address, device name, operating -system version, the configuration of the app when utilizing my Service, the time and date of your -use of the Service, and other statistics. - -# Cookies - -Cookies are files with a small amount of data that are commonly used as anonymous unique -identifiers. These are sent to your browser from the websites that you visit and are stored on your -device's internal memory. -This Service does not use these “cookies” explicitly. However, the app may use third-party code and -libraries that use “cookies” to collect information and improve their services. You have the option -to either accept or refuse these cookies and know when a cookie is being sent to your device. If you -choose to refuse our cookies, you may not be able to use some portions of this Service. - -# Service Providers - -I may employ third-party companies and individuals due to the following reasons: -To facilitate our Service; -To provide the Service on our behalf; -To perform Service-related services; or -To assist us in analyzing how our Service is used. -I want to inform users of this Service that these third parties have access to their Personal -Information. The reason is to perform the tasks assigned to them on our behalf. However, they are -obligated not to disclose or use the information for any other purpose. - -# Security - -I value your trust in providing us your Personal Information, thus we are striving to use -commercially acceptable means of protecting it. But remember that no method of transmission over the -internet, or method of electronic storage is 100% secure and reliable, and I cannot guarantee its -absolute security. - -# Links to Other Sites - -This Service may contain links to other sites. If you click on a third-party link, you will be -directed to that site. Note that these external sites are not operated by me. Therefore, I strongly -advise you to review the Privacy Policy of these websites. I have no control over and assume no -responsibility for the content, privacy policies, or practices of any third-party sites or services. - -# Children’s Privacy - -These Services do not address anyone under the age of 13. I do not knowingly collect personally -identifiable information from children under 13 years of age. In the case I discover that a child -under 13 has provided me with personal information, I immediately delete this from our servers. If -you are a parent or guardian and you are aware that your child has provided us with personal -information, please contact me so that I will be able to do the necessary actions. - -# Changes to This Privacy Policy - -I may update our Privacy Policy from time to time. Thus, you are advised to review this page -periodically for any changes. I will notify you of any changes by posting the new Privacy Policy on -this page. -This policy is effective as of 2022-08-30 - -# Contact Us - -If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact me at -yrkkh.cclub@gmail.com. +# Privacy Policy — PassKey: Password Manager + +_Last updated: 2026-09-16_ + +## Summary + +Your vault stays on your device. PassKey has no account, no sync and no server of its own. The +app does, however, bundle Google Firebase and Google Play libraries that send diagnostic and +usage data to Google. This document says exactly what does and does not leave your phone. + +## What we never collect + +PassKey does not collect, transmit or store the passwords, usernames, notes or any other entries +you save. They exist only in an encrypted database on your device. + +We cannot read your entries and we cannot recover them for you. There is no server-side copy. + +## What the app does send + +PassKey includes the following third-party components, all provided by Google: + +| Component | What it sends | +|---|---| +| Firebase Crashlytics | Crash stack traces, device model, OS version, app version, a Crashlytics installation identifier | +| Firebase Analytics | App usage events, session and screen information, device and installation identifiers, and an advertising ID (`AD_ID`) | +| Firebase Performance Monitoring | Startup, screen rendering and trace timings | +| Google Play in-app updates | Communicates with the Play Store to check for and install updates | + +Because of these components the app requests the `INTERNET` permission, along with +`ACCESS_NETWORK_STATE`, `WAKE_LOCK`, `AD_ID` and the Play AdServices attribution permissions. +None of these components has access to your vault contents. + +Data handled by these services is processed by Google under +[Google's privacy policy](https://policies.google.com/privacy). + +## Backups + +When you export a backup, the file is encrypted with AES-256-GCM using a key derived from a +password you choose, and written to the location you pick through the system file picker. + +That file is then yours to manage. Anyone who obtains **both** the file and its password can read +it. We keep no copy and cannot recover the password for you. + +## Device backup + +PassKey sets `android:allowBackup="false"` and excludes its data from both Android cloud backup +and device-to-device transfer, so your vault is not uploaded to your Google account. + +## Permissions + +| Permission | Why | +|---|---| +| `USE_BIOMETRIC` | Unlocking the vault | +| `INTERNET`, `ACCESS_NETWORK_STATE`, `WAKE_LOCK` | Required by the Firebase and Google Play libraries above | +| `AD_ID`, AdServices permissions | Merged in by Firebase Analytics | + +## Children + +PassKey is not directed at children and does not knowingly collect information from them. + +## Changes + +This policy may be updated. Material changes will be noted in the app's release notes and in this +file's history. + +## Contact + +yrkkh.cclub@gmail.com diff --git a/README.md b/README.md index add1e3d..281b932 100644 --- a/README.md +++ b/README.md @@ -30,22 +30,43 @@ design -> https://www.figma.com/community/file/1116675775484733517 ## 🤩 Features: -- Kotlin & Jetpack Compose. -- Clean Architecture. -- Kotlin Flows. -- Dark Mode Support. -- Biometric Authentication. -- Dagger-Hilt (Dependency Injection). -- DataStore Preferences. -- Encrypted Database. -- Screenshot Blocked. -- Open Source. -- Drag & Drop to reorder list. -- Swipe to Delete Feature. -- Separation of content. -- Import / Export data to and from storage. -- SQL Cipher (For Room DB Encryption and Decryption). -- Fully Offline : No internet permission required. +- Kotlin & Jetpack Compose, Material 3. +- Clean architecture with Kotlin Flows and Dagger-Hilt. +- Encrypted database (Room + SQLCipher). +- Biometric / device-credential unlock, with automatic re-lock on background and idle. +- Screenshot blocking, including dialogs and bottom sheets. +- Clipboard entries marked sensitive and cleared automatically. +- Password generator backed by SecureRandom. +- Vault analysis for weak and reused passwords. +- Password-protected encrypted backup and restore, written wherever you choose. +- Excluded from Android cloud backup and device-to-device transfer. +- Drag & drop reordering, swipe to delete, category separation. +- Dark mode and 17 languages. +- Open source. + +## 🔐 Security model: + +- The vault is a Room database encrypted with SQLCipher, stored only on the device. +- It is unlocked with device biometrics or the screen lock, and re-locks automatically after a + configurable idle period. +- Backups are encrypted with AES-256-GCM under a key derived from a password you choose + (PBKDF2-HMAC-SHA512), written through the system file picker. +- The app performs no network requests of its own and has no server or account. + +> **Known limitation, being addressed in 5.7.0:** the database key is currently a single +> build-time constant shared by every install, so the database is encrypted against someone who +> obtains the file alone, and not against someone who also has the APK. 5.7.0 replaces it with a +> per-install random key wrapped by an Android Keystore key and unlocked via a biometric +> `CryptoObject`, with a password-based recovery slot. + +## 🌐 Network and privacy: + +The app itself makes no network calls, but it bundles Google Firebase (Crashlytics, Analytics, +Performance Monitoring) and Google Play in-app updates. These require the `INTERNET` permission +and send crash diagnostics, usage events, performance traces and device/installation identifiers +— including an advertising ID — to Google. They never have access to vault contents. See +[Privacy Policy.md](Privacy%20Policy.md). + ## 🌎 Released Android Application: diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 062bc3e..a4f6a17 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.bhardwaj.passkey" minSdk = 28 targetSdk = 36 - versionCode = 44 - versionName = "5.5.2" + versionCode = 45 + versionName = "5.6.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 5fa2e9c..f3e1f53 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -139,4 +139,7 @@ مفتاح تسجيل الدخول + يحفظ PassKey بيانات تسجيل الدخول وكلمات المرور والملاحظات الخاصة الأخرى في قاعدة بيانات مشفّرة على جهازك.\n\nخزنتك لا تغادر هاتفك أبدًا. لا يوجد حساب ولا مزامنة ولا خادم لـ PassKey: التطبيق لا يجري أي طلبات شبكة من تلقاء نفسه، ولا تُرسل مدخلاتك إلى أي مكان.\n\nقاعدة البيانات مشفّرة بواسطة SQLCipher وتُفتح ببصمتك أو قفل شاشة جهازك. تُقفل الخزنة تلقائيًا عند مغادرة التطبيق، ولقطات الشاشة محظورة، وكلمات المرور المنسوخة تُعلَّم كحساسة وتُمسح من الحافظة بعد وقت قصير.\n\nيمكنك تنظيم المدخلات في فئات، وإعادة ترتيبها بالسحب، وإنشاء كلمات مرور قوية، وفحص خزنتك بحثًا عن كلمات مرور ضعيفة أو مكررة. تُصدَّر النسخ الاحتياطية كملف واحد مشفّر بكلمة مرور تختارها، ويمكن استعادتها على أي جهاز.\n\nPassKey مجاني ومفتوح المصدر، طوّره Aditya Bhardwaj. الشيفرة المصدرية الكاملة متاحة على GitHub. + تبقى خزنتك على جهازك.\n\nما لا نجمعه أبدًا: لا يجمع PassKey ولا ينقل ولا يخزّن كلمات المرور أو أسماء المستخدمين أو الملاحظات أو أي مدخلات تحفظها. توجد فقط في قاعدة بيانات مشفّرة على هاتفك. لا يمكننا قراءتها ولا استعادتها لك.\n\nما يرسله التطبيق: يتضمن PassKey خدمات Google Firebase Crashlytics وAnalytics وPerformance Monitoring، وتحديثات Google Play داخل التطبيق. ترسل هذه الخدمات إلى Google تشخيصات الأعطال وأحداث الاستخدام وبيانات الأداء ومعرّفات الجهاز والتثبيت بما في ذلك معرّف إعلاني. لا تصل هذه الخدمات إلى محتوى خزنتك إطلاقًا. بسبب هذه الخدمات يحتاج التطبيق إلى إذن الإنترنت، وتخضع معالجتها للبيانات لسياسة خصوصية Google.\n\nالنسخ الاحتياطية: عند التصدير يُشفَّر الملف بكلمة مرور تختارها ويُحفظ حيث تحدد للنظام. تصبح مسؤولية هذا الملف عليك بعد ذلك؛ من يحصل على الملف وكلمة مروره معًا يستطيع قراءته. نحن لا نحتفظ بأي نسخة.\n\nنسخ الجهاز الاحتياطي: PassKey مستثنى من النسخ الاحتياطي السحابي في Android ومن النقل بين الأجهزة، لذا لا تُرفع خزنتك إلى حساب Google الخاص بك.\n\nالتغييرات: قد تُحدَّث هذه السياسة؛ وستُذكر التغييرات الجوهرية في ملاحظات إصدار التطبيق.\n\nللاستفسار: تواصل عبر yrkkh.cclub@gmail.com. + باستخدامك PassKey فإنك توافق على ما يلي.\n\n1. PassKey برنامج مجاني مفتوح المصدر طوّره Aditya Bhardwaj.\n\n2. تُخزَّن خزنتك على جهازك فقط. لا توجد نسخة على أي خادم، وبالتالي لا توجد وسيلة لاستعادة بياناتك إذا فقدت جهازك أو أزلت التطبيق أو نسيت كلمة مرور النسخة الاحتياطية.\n\n3. الاحتفاظ بالنسخ الاحتياطية مسؤوليتك. صدّرها بانتظام واحفظ كلمة مرور النسخة في مكان آمن ومنفصل.\n\n4. يعتمد التطبيق على أمان البصمة أو قفل الشاشة في جهازك. إذا كان هذا القفل ضعيفًا أو مُزالًا أو مخترقًا، تقل الحماية التي يوفرها PassKey بالقدر نفسه.\n\n5. يتضمن PassKey خدمات Google Firebase وGoogle Play كما هو موضح في سياسة الخصوصية.\n\n6. يُقدَّم التطبيق \"كما هو\" دون أي ضمان صريح أو ضمني، بما في ذلك القابلية للتسويق والملاءمة لغرض معين.\n\n7. إلى الحد الذي يسمح به القانون، لا يتحمل المطوّر مسؤولية أي أضرار ناتجة عن استخدام التطبيق، بما في ذلك فقدان البيانات.\n\n8. يجوز تعديل التطبيق أو إيقافه في أي وقت. \ No newline at end of file diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index f8a9cfc..1da4261 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -135,4 +135,7 @@ চাবি লগইন + PassKey আপনার লগইন, পাসওয়ার্ড এবং অন্যান্য ব্যক্তিগত নোট আপনার ডিভাইসের একটি এনক্রিপ্ট করা ডেটাবেসে সংরক্ষণ করে।\n\nআপনার ভল্ট কখনও ফোন ছেড়ে যায় না। কোনো অ্যাকাউন্ট নেই, সিঙ্ক নেই, PassKey-এর কোনো সার্ভারও নেই: অ্যাপটি নিজে থেকে কোনো নেটওয়ার্ক অনুরোধ করে না এবং আপনার এন্ট্রি কোথাও পাঠানো হয় না।\n\nডেটাবেসটি SQLCipher দিয়ে এনক্রিপ্ট করা এবং আপনার ডিভাইসের বায়োমেট্রিক বা স্ক্রিন লক দিয়ে খোলে। অ্যাপ ছাড়লেই ভল্ট স্বয়ংক্রিয়ভাবে আবার লক হয়, স্ক্রিনশট বন্ধ, এবং কপি করা পাসওয়ার্ড সংবেদনশীল হিসেবে চিহ্নিত হয়ে অল্প পরেই ক্লিপবোর্ড থেকে মুছে যায়।\n\nআপনি এন্ট্রি বিভাগে সাজাতে, টেনে ক্রম বদলাতে, শক্তিশালী পাসওয়ার্ড তৈরি করতে এবং দুর্বল বা পুনরায় ব্যবহৃত পাসওয়ার্ড পরীক্ষা করতে পারেন। ব্যাকআপ একটি ফাইল হিসেবে রপ্তানি হয়, আপনার বাছাই করা পাসওয়ার্ড দিয়ে এনক্রিপ্ট করা, এবং যেকোনো ডিভাইসে পুনরুদ্ধার করা যায়।\n\nPassKey বিনামূল্যে ও ওপেন সোর্স, তৈরি করেছেন Aditya Bhardwaj। সম্পূর্ণ সোর্স কোড GitHub-এ পাওয়া যায়। + আপনার ভল্ট আপনার ডিভাইসেই থাকে।\n\nআমরা যা কখনও সংগ্রহ করি না: PassKey আপনার সংরক্ষিত পাসওয়ার্ড, ব্যবহারকারীর নাম, নোট বা অন্য কোনো এন্ট্রি সংগ্রহ, প্রেরণ বা সঞ্চয় করে না। সেগুলি কেবল আপনার ফোনের এনক্রিপ্ট করা ডেটাবেসে থাকে। আমরা সেগুলি পড়তেও পারি না, আপনার জন্য ফিরিয়ে আনতেও পারি না।\n\nঅ্যাপ যা পাঠায়: PassKey-তে Google Firebase Crashlytics, Analytics ও Performance Monitoring এবং Google Play ইন-অ্যাপ আপডেট অন্তর্ভুক্ত। এগুলি ক্র্যাশ ডায়াগনস্টিক, ব্যবহারের ঘটনা, কর্মক্ষমতার তথ্য এবং বিজ্ঞাপন আইডিসহ ডিভাইস ও ইনস্টলেশন শনাক্তকারী Google-কে পাঠায়। আপনার ভল্টের বিষয়বস্তুতে এদের কখনও প্রবেশাধিকার নেই। এই পরিষেবাগুলির কারণে অ্যাপটির ইন্টারনেট অনুমতি প্রয়োজন, এবং তাদের তথ্য ব্যবস্থাপনা Google-এর গোপনীয়তা নীতি দ্বারা পরিচালিত।\n\nব্যাকআপ: রপ্তানির সময় ফাইলটি আপনার বাছাই করা পাসওয়ার্ড দিয়ে এনক্রিপ্ট হয় এবং আপনি সিস্টেমকে যেখানে বলবেন সেখানে সংরক্ষিত হয়। এরপর সেই ফাইলের দায়িত্ব আপনার; যার কাছে ফাইল ও পাসওয়ার্ড দুটোই থাকবে সে পড়তে পারবে। আমরা কোনো অনুলিপি রাখি না।\n\nডিভাইস ব্যাকআপ: PassKey Android ক্লাউড ব্যাকআপ ও ডিভাইস-টু-ডিভাইস স্থানান্তর থেকে বাদ, তাই আপনার ভল্ট আপনার Google অ্যাকাউন্টে আপলোড হয় না।\n\nপরিবর্তন: এই নীতি হালনাগাদ হতে পারে; গুরুত্বপূর্ণ পরিবর্তন অ্যাপের রিলিজ নোটে জানানো হবে।\n\nপ্রশ্ন: যোগাযোগ করুন yrkkh.cclub@gmail.com। + PassKey ব্যবহার করে আপনি নিম্নলিখিত বিষয়ে সম্মত হচ্ছেন।\n\n১. PassKey একটি বিনামূল্যের, ওপেন-সোর্স সফটওয়্যার যা Aditya Bhardwaj তৈরি করেছেন।\n\n২. আপনার ভল্ট কেবল আপনার ডিভাইসে সংরক্ষিত। সার্ভারে কোনো অনুলিপি নেই, তাই ডিভাইস হারালে, অ্যাপ আনইনস্টল করলে বা ব্যাকআপ পাসওয়ার্ড ভুলে গেলে ডেটা ফেরানোর কোনো উপায় নেই।\n\n৩. ব্যাকআপ রাখা আপনার দায়িত্ব। নিয়মিত রপ্তানি করুন এবং ব্যাকআপ পাসওয়ার্ড নিরাপদ ও আলাদা জায়গায় রাখুন।\n\n৪. অ্যাপটি আপনার ডিভাইসের বায়োমেট্রিক বা স্ক্রিন-লক নিরাপত্তার উপর নির্ভরশীল। সেই লক দুর্বল, সরানো বা আপসকৃত হলে PassKey-এর সুরক্ষাও সেই অনুপাতে কমে যায়।\n\n৫. PassKey-তে গোপনীয়তা নীতিতে বর্ণিত Google Firebase ও Google Play পরিষেবা অন্তর্ভুক্ত।\n\n৬. অ্যাপ্লিকেশনটি \"যেমন আছে\" সরবরাহ করা হয়, কোনো প্রকার প্রকাশ্য বা অন্তর্নিহিত ওয়ারেন্টি ছাড়াই, যার মধ্যে বাণিজ্যিক উপযোগিতা ও নির্দিষ্ট উদ্দেশ্যের উপযুক্ততা অন্তর্ভুক্ত।\n\n৭. আইন অনুমোদিত সীমা পর্যন্ত, ডেভেলপার অ্যাপ্লিকেশন ব্যবহারের ফলে উদ্ভূত কোনো ক্ষতির জন্য, তথ্য হারানো সহ, দায়ী নন।\n\n৮. অ্যাপ্লিকেশনটি যেকোনো সময় পরিবর্তন বা বন্ধ করা হতে পারে। diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 117f6e4..58b1e66 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -136,4 +136,7 @@ zugangsdaten anmeldung + PassKey speichert deine Zugangsdaten, Passwörter und privaten Notizen in einer verschlüsselten Datenbank auf deinem Gerät.\n\nDein Tresor verlässt dein Telefon nie. Es gibt kein Konto, keine Synchronisierung und keinen PassKey-Server: Die App stellt keine eigenen Netzwerkanfragen, und deine Einträge werden nirgendwohin übertragen.\n\nDie Datenbank ist mit SQLCipher verschlüsselt und wird mit der Biometrie oder Bildschirmsperre deines Geräts entsperrt. Der Tresor sperrt sich automatisch, wenn du die App verlässt, Screenshots sind blockiert, und kopierte Passwörter werden als vertraulich markiert und kurz darauf aus der Zwischenablage gelöscht.\n\nDu kannst Einträge in Kategorien ordnen, sie per Ziehen neu anordnen, starke Passwörter erzeugen und deinen Tresor auf schwache oder mehrfach verwendete Passwörter prüfen. Backups werden als einzelne Datei exportiert, verschlüsselt mit einem Passwort deiner Wahl, und lassen sich auf jedem Gerät wiederherstellen.\n\nPassKey ist kostenlos und quelloffen, entwickelt von Aditya Bhardwaj. Der vollständige Quellcode ist auf GitHub verfügbar. + Dein Tresor bleibt auf deinem Gerät.\n\nWas wir nie erfassen: PassKey erfasst, überträgt oder speichert keine der Passwörter, Benutzernamen, Notizen oder sonstigen Einträge, die du sicherst. Sie liegen ausschließlich in einer verschlüsselten Datenbank auf deinem Telefon. Wir können sie weder lesen noch für dich wiederherstellen.\n\nWas die App sendet: PassKey enthält Google Firebase Crashlytics, Analytics und Performance Monitoring sowie Google Play In-App-Updates. Diese senden Absturzdiagnosen, Nutzungsereignisse, Leistungsdaten sowie Geräte- und Installationskennungen einschließlich einer Werbe-ID an Google. Sie haben nie Zugriff auf den Inhalt deines Tresors. Wegen dieser Dienste benötigt die App die Internetberechtigung; deren Datenverarbeitung unterliegt der Datenschutzerklärung von Google.\n\nBackups: Beim Export wird die Datei mit einem Passwort deiner Wahl verschlüsselt und dort abgelegt, wo du es dem System vorgibst. Diese Datei liegt dann in deiner Verantwortung; wer Datei und Passwort besitzt, kann sie lesen. Wir behalten keine Kopie.\n\nGerätesicherung: PassKey ist von der Android-Cloud-Sicherung und der Geräteübertragung ausgeschlossen, dein Tresor wird also nicht in dein Google-Konto hochgeladen.\n\nÄnderungen: Diese Erklärung kann aktualisiert werden; wesentliche Änderungen werden in den Versionshinweisen der App vermerkt.\n\nFragen: Kontakt yrkkh.cclub@gmail.com. + Mit der Nutzung von PassKey stimmst du Folgendem zu.\n\n1. PassKey ist kostenlose, quelloffene Software, entwickelt von Aditya Bhardwaj.\n\n2. Dein Tresor wird nur auf deinem Gerät gespeichert. Es gibt keine serverseitige Kopie und damit keine Möglichkeit, deine Daten wiederherzustellen, wenn du dein Gerät verlierst, die App deinstallierst oder ein Backup-Passwort vergisst.\n\n3. Backups liegen in deiner Verantwortung. Exportiere sie regelmäßig und bewahre das Backup-Passwort sicher und getrennt auf.\n\n4. Die App stützt sich auf die biometrische Sicherung oder Bildschirmsperre deines Geräts. Ist diese schwach, entfernt oder kompromittiert, verringert sich der Schutz durch PassKey entsprechend.\n\n5. PassKey enthält Google-Firebase- und Google-Play-Dienste wie in der Datenschutzerklärung beschrieben.\n\n6. Die Anwendung wird \"wie besehen\" bereitgestellt, ohne jegliche ausdrückliche oder stillschweigende Gewährleistung, einschließlich Marktgängigkeit und Eignung für einen bestimmten Zweck.\n\n7. Soweit gesetzlich zulässig, haftet der Entwickler nicht für Schäden aus der Nutzung der Anwendung, einschließlich Datenverlust.\n\n8. Die Anwendung kann jederzeit geändert oder eingestellt werden. \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2547279..78136fd 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -136,4 +136,7 @@ llave acceso + PassKey guarda tus inicios de sesión, contraseñas y otras notas privadas en una base de datos cifrada en tu dispositivo.\n\nTu caja fuerte nunca sale del teléfono. No hay cuenta, ni sincronización, ni servidor de PassKey: la aplicación no realiza peticiones de red propias y tus entradas nunca se transmiten a ninguna parte.\n\nLa base de datos se cifra con SQLCipher y se desbloquea con la biometría o el bloqueo de pantalla de tu dispositivo. La caja fuerte se vuelve a bloquear automáticamente al salir de la aplicación, las capturas de pantalla están bloqueadas y las contraseñas copiadas se marcan como sensibles y se borran del portapapeles poco después.\n\nPuedes organizar las entradas en categorías, reordenarlas arrastrando, generar contraseñas seguras y revisar tu caja fuerte en busca de contraseñas débiles o repetidas. Las copias de seguridad se exportan como un único archivo cifrado con una contraseña que tú eliges y pueden restaurarse en cualquier dispositivo.\n\nPassKey es gratuita y de código abierto, desarrollada por Aditya Bhardwaj. El código fuente completo está disponible en GitHub. + Tu caja fuerte permanece en tu dispositivo.\n\nLo que nunca recopilamos: PassKey no recopila, transmite ni almacena las contraseñas, nombres de usuario, notas ni ninguna otra entrada que guardes. Solo existen en una base de datos cifrada en tu teléfono. No podemos leerlas ni recuperarlas por ti.\n\nLo que sí envía la aplicación: PassKey incluye Google Firebase Crashlytics, Analytics y Performance Monitoring, y las actualizaciones integradas de Google Play. Estos servicios envían diagnósticos de fallos, eventos de uso, trazas de rendimiento e identificadores de dispositivo e instalación, incluido un identificador de publicidad, a Google. Nunca tienen acceso al contenido de tu caja fuerte. Por estos servicios la aplicación requiere el permiso de internet, y su tratamiento de datos se rige por la política de privacidad de Google.\n\nCopias de seguridad: al exportar una copia, el archivo se cifra con una contraseña que tú eliges y se guarda donde le indiques al sistema. Ese archivo queda bajo tu responsabilidad; cualquiera que obtenga el archivo y su contraseña podrá leerlo. No conservamos ninguna copia.\n\nCopia del dispositivo: PassKey está excluida de la copia de seguridad en la nube de Android y de la transferencia entre dispositivos, así que tu caja fuerte no se sube a tu cuenta de Google.\n\nCambios: esta política puede actualizarse; los cambios importantes se indicarán en las notas de versión de la aplicación.\n\nDudas: escribe a yrkkh.cclub@gmail.com. + Al usar PassKey aceptas lo siguiente.\n\n1. PassKey es software gratuito y de código abierto desarrollado por Aditya Bhardwaj.\n\n2. Tu caja fuerte se almacena únicamente en tu dispositivo. No hay copia en servidor y, por tanto, no existe forma de recuperar tus datos si pierdes el dispositivo, desinstalas la aplicación u olvidas una contraseña de copia de seguridad.\n\n3. Mantener copias de seguridad es responsabilidad tuya. Expórtalas con regularidad y guarda la contraseña de la copia en un lugar seguro y separado.\n\n4. La aplicación depende de la seguridad biométrica o del bloqueo de pantalla de tu dispositivo. Si ese bloqueo es débil, se elimina o queda comprometido, la protección que ofrece PassKey se reduce en consecuencia.\n\n5. PassKey incluye servicios de Google Firebase y Google Play según se describe en la Política de Privacidad.\n\n6. La aplicación se proporciona \"tal cual\", sin garantía de ningún tipo, expresa o implícita, incluidas las de comerciabilidad e idoneidad para un fin determinado.\n\n7. En la medida permitida por la ley, el desarrollador no se hace responsable de los daños derivados del uso de la aplicación, incluida la pérdida de datos.\n\n8. La aplicación puede modificarse o descontinuarse en cualquier momento. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c819cbf..d246b44 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -136,4 +136,7 @@ identifiant connexion + PassKey enregistre vos identifiants, mots de passe et autres notes privées dans une base de données chiffrée sur votre appareil.\n\nVotre coffre ne quitte jamais votre téléphone. Il n\'y a ni compte, ni synchronisation, ni serveur PassKey : l\'application n\'effectue aucune requête réseau de son propre fait et vos entrées ne sont transmises nulle part.\n\nLa base de données est chiffrée avec SQLCipher et déverrouillée par la biométrie ou le verrouillage d\'écran de votre appareil. Le coffre se reverrouille automatiquement lorsque vous quittez l\'application, les captures d\'écran sont bloquées, et les mots de passe copiés sont marqués comme sensibles puis effacés du presse-papiers peu après.\n\nVous pouvez classer vos entrées par catégories, les réorganiser par glissement, générer des mots de passe robustes et vérifier votre coffre à la recherche de mots de passe faibles ou réutilisés. Les sauvegardes sont exportées sous forme d\'un fichier unique chiffré avec un mot de passe de votre choix, restaurable sur n\'importe quel appareil.\n\nPassKey est gratuite et open source, développée par Aditya Bhardwaj. Le code source complet est disponible sur GitHub. + Votre coffre reste sur votre appareil.\n\nCe que nous ne collectons jamais : PassKey ne collecte, ne transmet et ne stocke aucun des mots de passe, identifiants, notes ou autres entrées que vous enregistrez. Ils n\'existent que dans une base de données chiffrée sur votre téléphone. Nous ne pouvons ni les lire, ni les récupérer pour vous.\n\nCe que l\'application envoie : PassKey intègre Google Firebase Crashlytics, Analytics et Performance Monitoring, ainsi que les mises à jour intégrées de Google Play. Ces services envoient à Google des diagnostics de plantage, des événements d\'utilisation, des traces de performance et des identifiants d\'appareil et d\'installation, dont un identifiant publicitaire. Ils n\'ont jamais accès au contenu de votre coffre. En raison de ces services, l\'application requiert l\'autorisation d\'accès à Internet, et leur traitement des données relève de la politique de confidentialité de Google.\n\nSauvegardes : lorsque vous exportez une sauvegarde, le fichier est chiffré avec un mot de passe de votre choix et enregistré à l\'emplacement que vous indiquez au système. Ce fichier relève ensuite de votre responsabilité ; quiconque obtient à la fois le fichier et son mot de passe peut le lire. Nous n\'en conservons aucune copie.\n\nSauvegarde de l\'appareil : PassKey est exclue de la sauvegarde cloud Android et du transfert entre appareils, votre coffre n\'est donc pas envoyé vers votre compte Google.\n\nModifications : cette politique peut évoluer ; les changements importants seront signalés dans les notes de version.\n\nQuestions : contactez yrkkh.cclub@gmail.com. + En utilisant PassKey, vous acceptez ce qui suit.\n\n1. PassKey est un logiciel gratuit et open source développé par Aditya Bhardwaj.\n\n2. Votre coffre est stocké uniquement sur votre appareil. Il n\'existe aucune copie côté serveur, et donc aucun moyen de récupérer vos données si vous perdez votre appareil, désinstallez l\'application ou oubliez un mot de passe de sauvegarde.\n\n3. La conservation des sauvegardes vous incombe. Exportez-les régulièrement et conservez le mot de passe de sauvegarde en lieu sûr et séparé.\n\n4. L\'application repose sur la sécurité biométrique ou le verrouillage d\'écran de votre appareil. Si ce verrouillage est faible, supprimé ou compromis, la protection offerte par PassKey est réduite d\'autant.\n\n5. PassKey intègre les services Google Firebase et Google Play décrits dans la politique de confidentialité.\n\n6. L\'application est fournie \"en l\'état\", sans garantie d\'aucune sorte, expresse ou implicite, y compris de qualité marchande et d\'adéquation à un usage particulier.\n\n7. Dans la mesure permise par la loi, le développeur n\'est pas responsable des dommages résultant de l\'utilisation de l\'application, y compris la perte de données.\n\n8. L\'application peut être modifiée ou abandonnée à tout moment. diff --git a/app/src/main/res/values-gu/strings.xml b/app/src/main/res/values-gu/strings.xml index b7e07d6..b2e33fe 100644 --- a/app/src/main/res/values-gu/strings.xml +++ b/app/src/main/res/values-gu/strings.xml @@ -134,4 +134,7 @@ ચાવી લૉગિન + PassKey તમારા લૉગિન, પાસવર્ડ અને અન્ય ખાનગી નોંધો તમારા ઉપકરણ પરના એન્ક્રિપ્ટેડ ડેટાબેઝમાં સાચવે છે.\n\nતમારી વૉલ્ટ ક્યારેય તમારા ફોનની બહાર જતી નથી. કોઈ ખાતું નથી, સિંક નથી, PassKey નું કોઈ સર્વર પણ નથી: ઍપ પોતે કોઈ નેટવર્ક વિનંતી કરતી નથી અને તમારી નોંધો ક્યાંય મોકલાતી નથી.\n\nડેટાબેઝ SQLCipher થી એન્ક્રિપ્ટ થયેલો છે અને તમારા ઉપકરણના બાયોમેટ્રિક કે સ્ક્રીન લૉકથી ખૂલે છે. ઍપ છોડતાં જ વૉલ્ટ આપોઆપ ફરી લૉક થાય છે, સ્ક્રીનશૉટ રોકાયેલા છે, અને કૉપિ કરેલા પાસવર્ડ સંવેદનશીલ તરીકે ચિહ્નિત થઈને થોડી વારમાં ક્લિપબોર્ડમાંથી દૂર થાય છે.\n\nતમે નોંધોને શ્રેણીઓમાં ગોઠવી શકો, ખેંચીને ક્રમ બદલી શકો, મજબૂત પાસવર્ડ બનાવી શકો અને નબળા કે પુનરાવર્તિત પાસવર્ડ તપાસી શકો. બૅકઅપ એક જ ફાઇલ તરીકે નિકાસ થાય છે, તમે પસંદ કરેલા પાસવર્ડથી એન્ક્રિપ્ટેડ, અને કોઈ પણ ઉપકરણ પર પુનઃસ્થાપિત થઈ શકે છે.\n\nPassKey મફત અને ઓપન સોર્સ છે, Aditya Bhardwaj દ્વારા વિકસાવાયેલ. સંપૂર્ણ સ્રોત કોડ GitHub પર ઉપલબ્ધ છે. + તમારી વૉલ્ટ તમારા ઉપકરણ પર જ રહે છે.\n\nઅમે ક્યારેય શું એકત્ર કરતા નથી: PassKey તમે સાચવેલા પાસવર્ડ, વપરાશકર્તાનામ, નોંધો કે અન્ય કોઈ પણ નોંધ એકત્ર, પ્રસારિત કે સંગ્રહિત કરતું નથી. તે માત્ર તમારા ફોનના એન્ક્રિપ્ટેડ ડેટાબેઝમાં રહે છે. અમે તે વાંચી શકતા નથી અને તમારા માટે પાછી મેળવી પણ શકતા નથી.\n\nઍપ શું મોકલે છે: PassKey માં Google Firebase Crashlytics, Analytics અને Performance Monitoring તથા Google Play ઇન-ઍપ અપડેટ સામેલ છે. આ ક્રેશ નિદાન, વપરાશ ઘટનાઓ, કામગીરી માહિતી અને જાહેરાત ID સહિત ઉપકરણ તથા ઇન્સ્ટોલેશન ઓળખકર્તાઓ Google ને મોકલે છે. તેમને તમારી વૉલ્ટની સામગ્રી સુધી ક્યારેય પ્રવેશ મળતો નથી. આ સેવાઓને કારણે ઍપને ઇન્ટરનેટ પરવાનગી જોઈએ છે, અને તેમનું ડેટા સંચાલન Google ની ગોપનીયતા નીતિ હેઠળ આવે છે.\n\nબૅકઅપ: નિકાસ વખતે ફાઇલ તમે પસંદ કરેલા પાસવર્ડથી એન્ક્રિપ્ટ થાય છે અને તમે સિસ્ટમને જ્યાં કહો ત્યાં સચવાય છે. ત્યાર બાદ તે ફાઇલની જવાબદારી તમારી છે; જેની પાસે ફાઇલ અને પાસવર્ડ બંને હોય તે તેને વાંચી શકે છે. અમે કોઈ નકલ રાખતા નથી.\n\nઉપકરણ બૅકઅપ: PassKey ને Android ક્લાઉડ બૅકઅપ અને ઉપકરણ-થી-ઉપકરણ સ્થાનાંતરણમાંથી બાકાત રાખ્યું છે, તેથી તમારી વૉલ્ટ તમારા Google ખાતામાં અપલોડ થતી નથી.\n\nફેરફારો: આ નીતિ અપડેટ થઈ શકે છે; મહત્ત્વના ફેરફારો ઍપની રિલીઝ નોંધમાં જણાવાશે.\n\nપ્રશ્નો: સંપર્ક yrkkh.cclub@gmail.com. + PassKey વાપરીને તમે નીચેની બાબતો સાથે સંમત થાઓ છો.\n\n1. PassKey એ Aditya Bhardwaj દ્વારા વિકસાવાયેલ મફત, ઓપન-સોર્સ સોફ્ટવેર છે.\n\n2. તમારી વૉલ્ટ માત્ર તમારા ઉપકરણ પર સંગ્રહિત છે. સર્વર પર કોઈ નકલ નથી, તેથી ઉપકરણ ખોવાય, ઍપ દૂર કરો કે બૅકઅપ પાસવર્ડ ભૂલી જાઓ તો ડેટા પાછો મેળવવાનો કોઈ માર્ગ નથી.\n\n3. બૅકઅપ રાખવાની જવાબદારી તમારી છે. નિયમિત નિકાસ કરો અને બૅકઅપ પાસવર્ડ સુરક્ષિત, અલગ જગ્યાએ રાખો.\n\n4. ઍપ તમારા ઉપકરણની બાયોમેટ્રિક કે સ્ક્રીન-લૉક સુરક્ષા પર આધારિત છે. જો તે લૉક નબળો, દૂર કરેલો કે ભંગ થયેલો હોય, તો PassKey નું રક્ષણ તે પ્રમાણમાં ઘટે છે.\n\n5. PassKey માં ગોપનીયતા નીતિમાં વર્ણવેલ Google Firebase અને Google Play સેવાઓ સામેલ છે.\n\n6. આ એપ્લિકેશન \"જેમ છે તેમ\" પૂરી પાડવામાં આવે છે, કોઈ પણ સ્પષ્ટ કે ગર્ભિત વોરંટી વિના, જેમાં વેપારયોગ્યતા અને ચોક્કસ હેતુ માટેની યોગ્યતા સામેલ છે.\n\n7. કાયદા દ્વારા માન્ય હદ સુધી, ડેવલપર એપ્લિકેશનના ઉપયોગથી થતા કોઈ પણ નુકસાન માટે, ડેટા ગુમાવવા સહિત, જવાબદાર નથી.\n\n8. એપ્લિકેશન કોઈ પણ સમયે સુધારી કે બંધ કરી શકાય છે. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 1c90f86..94bb73a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -135,4 +135,7 @@ कुंजी लॉगिन + PassKey आपके लॉगिन, पासवर्ड और अन्य निजी नोट्स को आपके डिवाइस पर एक एन्क्रिप्टेड डेटाबेस में सहेजता है।\n\nआपकी वॉल्ट कभी आपके फ़ोन से बाहर नहीं जाती। न कोई खाता है, न सिंक, न PassKey का कोई सर्वर: ऐप अपनी ओर से कोई नेटवर्क अनुरोध नहीं करता और आपकी प्रविष्टियाँ कहीं नहीं भेजी जातीं।\n\nडेटाबेस SQLCipher से एन्क्रिप्ट किया गया है और आपके डिवाइस के बायोमेट्रिक या स्क्रीन लॉक से खुलता है। ऐप छोड़ते ही वॉल्ट अपने आप फिर लॉक हो जाती है, स्क्रीनशॉट रोक दिए गए हैं, और कॉपी किए गए पासवर्ड संवेदनशील चिह्नित होकर कुछ ही देर में क्लिपबोर्ड से हट जाते हैं।\n\nआप प्रविष्टियों को श्रेणियों में रख सकते हैं, खींचकर क्रम बदल सकते हैं, मज़बूत पासवर्ड बना सकते हैं और कमज़ोर या दोहराए गए पासवर्ड जाँच सकते हैं। बैकअप एक ही फ़ाइल के रूप में निर्यात होता है, जो आपके चुने पासवर्ड से एन्क्रिप्टेड होता है और किसी भी डिवाइस पर बहाल किया जा सकता है।\n\nPassKey निःशुल्क और ओपन सोर्स है, जिसे Aditya Bhardwaj ने बनाया है। पूरा स्रोत कोड GitHub पर उपलब्ध है। + आपकी वॉल्ट आपके डिवाइस पर ही रहती है।\n\nहम कभी क्या एकत्र नहीं करते: PassKey आपके सहेजे गए पासवर्ड, उपयोगकर्ता नाम, नोट्स या किसी भी प्रविष्टि को एकत्र, प्रेषित या संग्रहीत नहीं करता। ये केवल आपके फ़ोन के एन्क्रिप्टेड डेटाबेस में रहते हैं। हम न इन्हें पढ़ सकते हैं, न आपके लिए वापस ला सकते हैं।\n\nऐप क्या भेजता है: PassKey में Google Firebase Crashlytics, Analytics और Performance Monitoring तथा Google Play इन-ऐप अपडेट शामिल हैं। ये क्रैश डायग्नोस्टिक्स, उपयोग इवेंट, प्रदर्शन डेटा और विज्ञापन आईडी सहित डिवाइस व इंस्टॉलेशन पहचानकर्ता Google को भेजते हैं। इनकी पहुँच आपकी वॉल्ट की सामग्री तक कभी नहीं होती। इन सेवाओं के कारण ऐप को इंटरनेट अनुमति चाहिए, और इनका डेटा प्रबंधन Google की गोपनीयता नीति से संचालित होता है।\n\nबैकअप: निर्यात करते समय फ़ाइल आपके चुने पासवर्ड से एन्क्रिप्ट होती है और वहीं सहेजी जाती है जहाँ आप सिस्टम को बताते हैं। उसके बाद उस फ़ाइल की ज़िम्मेदारी आपकी है; जिसके पास फ़ाइल और पासवर्ड दोनों हों वह उसे पढ़ सकता है। हम कोई प्रति नहीं रखते।\n\nडिवाइस बैकअप: PassKey को Android क्लाउड बैकअप और डिवाइस-टू-डिवाइस ट्रांसफ़र से बाहर रखा गया है, इसलिए आपकी वॉल्ट आपके Google खाते में अपलोड नहीं होती।\n\nबदलाव: यह नीति अद्यतन हो सकती है; महत्वपूर्ण बदलाव ऐप के रिलीज़ नोट्स में बताए जाएँगे।\n\nप्रश्न: संपर्क करें yrkkh.cclub@gmail.com। + PassKey का उपयोग करके आप निम्नलिखित से सहमत होते हैं।\n\n1. PassKey एक निःशुल्क, ओपन-सोर्स सॉफ़्टवेयर है जिसे Aditya Bhardwaj ने विकसित किया है।\n\n2. आपकी वॉल्ट केवल आपके डिवाइस पर संग्रहीत होती है। सर्वर पर कोई प्रति नहीं है, इसलिए डिवाइस खोने, ऐप हटाने या बैकअप पासवर्ड भूलने पर डेटा वापस पाने का कोई उपाय नहीं है।\n\n3. बैकअप रखना आपकी ज़िम्मेदारी है। इन्हें नियमित रूप से निर्यात करें और बैकअप पासवर्ड को सुरक्षित, अलग जगह रखें।\n\n4. ऐप आपके डिवाइस की बायोमेट्रिक या स्क्रीन-लॉक सुरक्षा पर निर्भर करता है। यदि वह लॉक कमज़ोर, हटाया हुआ या असुरक्षित है, तो PassKey द्वारा दी गई सुरक्षा उसी अनुपात में घट जाती है।\n\n5. PassKey में गोपनीयता नीति में वर्णित Google Firebase और Google Play सेवाएँ शामिल हैं।\n\n6. यह एप्लिकेशन \"जैसा है\" प्रदान किया जाता है, बिना किसी व्यक्त या निहित वारंटी के, जिसमें व्यापारिकता और किसी विशेष उद्देश्य के लिए उपयुक्तता शामिल है।\n\n7. कानून द्वारा अनुमत सीमा तक, डेवलपर एप्लिकेशन के उपयोग से उत्पन्न किसी भी क्षति, जिसमें डेटा हानि शामिल है, के लिए उत्तरदायी नहीं है।\n\n8. एप्लिकेशन को किसी भी समय संशोधित या बंद किया जा सकता है। \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b92df5a..48b2a51 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -136,4 +136,7 @@ chiave accesso + PassKey salva credenziali, password e altre note private in un database cifrato sul tuo dispositivo.\n\nLa tua cassaforte non lascia mai il telefono. Non c\'è alcun account, nessuna sincronizzazione e nessun server PassKey: l\'app non effettua richieste di rete proprie e le tue voci non vengono trasmesse da nessuna parte.\n\nIl database è cifrato con SQLCipher e si sblocca con la biometria o il blocco schermo del dispositivo. La cassaforte si riblocca automaticamente quando esci dall\'app, gli screenshot sono bloccati e le password copiate vengono contrassegnate come sensibili ed eliminate dagli appunti poco dopo.\n\nPuoi organizzare le voci in categorie, riordinarle trascinando, generare password robuste e controllare la cassaforte alla ricerca di password deboli o riutilizzate. I backup vengono esportati come un unico file cifrato con una password scelta da te e si possono ripristinare su qualsiasi dispositivo.\n\nPassKey è gratuita e open source, sviluppata da Aditya Bhardwaj. Il codice sorgente completo è disponibile su GitHub. + La tua cassaforte resta sul tuo dispositivo.\n\nCosa non raccogliamo mai: PassKey non raccoglie, trasmette né memorizza le password, i nomi utente, le note o qualsiasi altra voce che salvi. Esistono soltanto in un database cifrato sul tuo telefono. Non possiamo leggerle né recuperarle per te.\n\nCosa invia l\'app: PassKey include Google Firebase Crashlytics, Analytics e Performance Monitoring e gli aggiornamenti in-app di Google Play. Questi servizi inviano a Google diagnostiche di arresto anomalo, eventi di utilizzo, tracce prestazionali e identificatori di dispositivo e installazione, compreso un ID pubblicitario. Non hanno mai accesso al contenuto della cassaforte. A causa di questi servizi l\'app richiede l\'autorizzazione a Internet, e il loro trattamento dei dati è regolato dall\'informativa privacy di Google.\n\nBackup: quando esporti un backup, il file viene cifrato con una password scelta da te e salvato dove indichi al sistema. Quel file è poi sotto la tua responsabilità; chiunque ottenga sia il file sia la password può leggerlo. Non ne conserviamo copia.\n\nBackup del dispositivo: PassKey è esclusa dal backup cloud di Android e dal trasferimento tra dispositivi, quindi la cassaforte non viene caricata sul tuo account Google.\n\nModifiche: questa informativa può essere aggiornata; le modifiche rilevanti saranno indicate nelle note di versione.\n\nDomande: scrivi a yrkkh.cclub@gmail.com. + Usando PassKey accetti quanto segue.\n\n1. PassKey è software gratuito e open source sviluppato da Aditya Bhardwaj.\n\n2. La tua cassaforte è archiviata solo sul tuo dispositivo. Non esiste copia lato server e quindi non c\'è modo di recuperare i dati se perdi il dispositivo, disinstalli l\'app o dimentichi una password di backup.\n\n3. Conservare i backup è una tua responsabilità. Esportali regolarmente e custodisci la password del backup in un luogo sicuro e separato.\n\n4. L\'app si appoggia alla sicurezza biometrica o al blocco schermo del dispositivo. Se tale blocco è debole, rimosso o compromesso, la protezione offerta da PassKey si riduce di conseguenza.\n\n5. PassKey include i servizi Google Firebase e Google Play descritti nell\'Informativa sulla privacy.\n\n6. L\'applicazione è fornita \"così com\'è\", senza garanzie di alcun tipo, esplicite o implicite, comprese quelle di commerciabilità e idoneità a uno scopo particolare.\n\n7. Nei limiti consentiti dalla legge, lo sviluppatore non è responsabile per danni derivanti dall\'uso dell\'applicazione, inclusa la perdita di dati.\n\n8. L\'applicazione può essere modificata o dismessa in qualsiasi momento. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 9ee1dd5..9808cb5 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -133,4 +133,7 @@ 鍵 ログイン + PassKey は、ログイン情報・パスワード・その他の非公開メモを、お使いの端末上の暗号化データベースに保存します。\n\n保管庫が端末外に出ることはありません。アカウントも同期も PassKey のサーバーも存在せず、アプリ自身はネットワーク通信を行わないため、登録内容がどこかへ送信されることはありません。\n\nデータベースは SQLCipher で暗号化され、端末の生体認証または画面ロックで解錠します。アプリを離れると保管庫は自動的に再ロックされ、スクリーンショットは禁止され、コピーしたパスワードは機密として扱われ、まもなくクリップボードから消去されます。\n\n項目はカテゴリ分けやドラッグによる並べ替えができ、強力なパスワードの生成や、脆弱・使い回しパスワードの点検も行えます。バックアップは、ご自身で決めたパスワードで暗号化した単一のファイルとして書き出され、どの端末でも復元できます。\n\nPassKey は Aditya Bhardwaj が開発した無料のオープンソースソフトウェアで、ソースコードはすべて GitHub で公開されています。 + 保管庫は端末内に留まります。\n\n収集しないもの: PassKey は、保存されたパスワード・ユーザー名・メモなどの項目を一切収集・送信・保管しません。それらは端末上の暗号化データベースにのみ存在します。当方が内容を読むことも、復元することもできません。\n\nアプリが送信するもの: PassKey には Google Firebase Crashlytics、Analytics、Performance Monitoring、および Google Play のアプリ内更新が含まれます。これらはクラッシュ診断、利用イベント、パフォーマンス情報、広告 ID を含む端末およびインストール識別子を Google に送信します。保管庫の中身にアクセスすることはありません。これらのサービスのため、アプリはインターネット権限を必要とし、そのデータの取り扱いは Google のプライバシーポリシーに従います。\n\nバックアップ: 書き出したファイルはご自身で決めたパスワードで暗号化され、システムで指定した場所に保存されます。以後そのファイルの管理はご自身の責任となり、ファイルとパスワードの双方を入手した者は内容を読むことができます。当方は控えを保持しません。\n\n端末バックアップ: PassKey は Android のクラウドバックアップおよび端末間転送の対象外です。保管庫が Google アカウントへアップロードされることはありません。\n\n変更: 本ポリシーは更新される場合があります。重要な変更はリリースノートに記載します。\n\nお問い合わせ: yrkkh.cclub@gmail.com までご連絡ください。 + PassKey を利用することで、以下に同意したものとみなされます。\n\n1. PassKey は Aditya Bhardwaj が開発した無料のオープンソースソフトウェアです。\n\n2. 保管庫は端末上にのみ保存されます。サーバー側の控えは存在しないため、端末の紛失、アプリの削除、バックアップパスワードの失念があった場合にデータを復元する手段はありません。\n\n3. バックアップの保持は利用者の責任です。定期的に書き出し、バックアップパスワードは安全な別の場所に保管してください。\n\n4. 本アプリは端末の生体認証または画面ロックに依存します。そのロックが脆弱、解除済み、または侵害されている場合、PassKey による保護もその分低下します。\n\n5. PassKey にはプライバシーポリシーに記載の Google Firebase および Google Play のサービスが含まれます。\n\n6. 本アプリケーションは「現状有姿」で提供され、商品性および特定目的への適合性を含め、明示黙示を問わずいかなる保証も行いません。\n\n7. 法令が許す範囲において、開発者はデータの喪失を含む本アプリケーションの利用に起因する損害について責任を負いません。\n\n8. 本アプリケーションは予告なく変更または提供終了となる場合があります。 \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 9931faf..522cd50 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -134,4 +134,7 @@ 키 로그인 + PassKey는 로그인 정보, 비밀번호, 그 밖의 비공개 메모를 기기 안의 암호화된 데이터베이스에 저장합니다.\n\n보관함은 기기를 벗어나지 않습니다. 계정도, 동기화도, PassKey 서버도 없습니다. 앱 자체는 네트워크 요청을 하지 않으며 저장한 항목이 어디로도 전송되지 않습니다.\n\n데이터베이스는 SQLCipher로 암호화되며 기기의 생체 인증이나 화면 잠금으로 해제합니다. 앱을 벗어나면 보관함이 자동으로 다시 잠기고, 스크린샷은 차단되며, 복사한 비밀번호는 민감 정보로 표시된 뒤 잠시 후 클립보드에서 지워집니다.\n\n항목을 카테고리로 정리하고 끌어서 순서를 바꿀 수 있으며, 강력한 비밀번호를 생성하고 취약하거나 재사용된 비밀번호를 점검할 수 있습니다. 백업은 직접 정한 비밀번호로 암호화된 단일 파일로 내보내지며 어떤 기기에서도 복원할 수 있습니다.\n\nPassKey는 Aditya Bhardwaj가 개발한 무료 오픈소스 소프트웨어이며 전체 소스 코드는 GitHub에서 공개되어 있습니다. + 보관함은 기기에 남아 있습니다.\n\n수집하지 않는 것: PassKey는 저장한 비밀번호, 사용자 이름, 메모 등 어떤 항목도 수집하거나 전송하거나 보관하지 않습니다. 해당 정보는 기기의 암호화된 데이터베이스에만 존재합니다. 저희는 이를 읽을 수도, 대신 복구해 드릴 수도 없습니다.\n\n앱이 전송하는 것: PassKey에는 Google Firebase Crashlytics, Analytics, Performance Monitoring과 Google Play 인앱 업데이트가 포함됩니다. 이들은 비정상 종료 진단, 사용 이벤트, 성능 기록, 광고 ID를 포함한 기기 및 설치 식별자를 Google로 전송합니다. 보관함 내용에는 접근하지 않습니다. 이러한 서비스 때문에 앱은 인터넷 권한을 필요로 하며, 해당 데이터 처리는 Google의 개인정보처리방침을 따릅니다.\n\n백업: 백업을 내보내면 직접 정한 비밀번호로 파일이 암호화되어 지정한 위치에 저장됩니다. 이후 그 파일은 사용자의 책임이며, 파일과 비밀번호를 모두 가진 사람은 내용을 읽을 수 있습니다. 저희는 사본을 보관하지 않습니다.\n\n기기 백업: PassKey는 Android 클라우드 백업과 기기 간 전송에서 제외되어 보관함이 Google 계정에 업로드되지 않습니다.\n\n변경: 본 방침은 변경될 수 있으며, 중요한 변경은 앱 출시 노트에 기재됩니다.\n\n문의: yrkkh.cclub@gmail.com 로 연락해 주세요. + PassKey를 사용함으로써 다음에 동의하게 됩니다.\n\n1. PassKey는 Aditya Bhardwaj가 개발한 무료 오픈소스 소프트웨어입니다.\n\n2. 보관함은 기기에만 저장됩니다. 서버 사본이 없으므로 기기를 분실하거나 앱을 삭제하거나 백업 비밀번호를 잊은 경우 데이터를 복구할 방법이 없습니다.\n\n3. 백업 보관은 사용자의 책임입니다. 정기적으로 내보내고 백업 비밀번호는 안전한 별도 장소에 보관하세요.\n\n4. 앱은 기기의 생체 인증 또는 화면 잠금 보안에 의존합니다. 해당 잠금이 취약하거나 해제되거나 침해되면 PassKey가 제공하는 보호도 그만큼 약해집니다.\n\n5. PassKey에는 개인정보처리방침에 기술된 Google Firebase 및 Google Play 서비스가 포함됩니다.\n\n6. 본 애플리케이션은 상품성 및 특정 목적 적합성을 포함하여 명시적이든 묵시적이든 어떠한 보증도 없이 \"있는 그대로\" 제공됩니다.\n\n7. 법이 허용하는 범위에서 개발자는 데이터 손실을 포함해 애플리케이션 사용으로 발생한 손해에 대해 책임지지 않습니다.\n\n8. 애플리케이션은 언제든 변경되거나 중단될 수 있습니다. \ No newline at end of file diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 5366c0d..1ebde8c 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -135,4 +135,7 @@ किल्ली लॉगिन + PassKey तुमचे लॉगिन, पासवर्ड आणि इतर खाजगी नोंदी तुमच्या उपकरणावरील एन्क्रिप्टेड डेटाबेसमध्ये साठवते.\n\nतुमची व्हॉल्ट कधीही फोनबाहेर जात नाही. खाते नाही, सिंक नाही, PassKey चा सर्व्हरही नाही: ॲप स्वतःहून कोणतीही नेटवर्क विनंती करत नाही आणि तुमच्या नोंदी कुठेही पाठवल्या जात नाहीत.\n\nडेटाबेस SQLCipher ने एन्क्रिप्ट केलेला असून तुमच्या उपकरणाच्या बायोमेट्रिक किंवा स्क्रीन लॉकने उघडतो. ॲप सोडताच व्हॉल्ट आपोआप पुन्हा लॉक होते, स्क्रीनशॉट रोखलेले आहेत, आणि कॉपी केलेले पासवर्ड संवेदनशील म्हणून चिन्हांकित होऊन थोड्याच वेळात क्लिपबोर्डमधून हटवले जातात.\n\nनोंदी श्रेणींमध्ये मांडता येतात, ओढून क्रम बदलता येतो, मजबूत पासवर्ड तयार करता येतात आणि कमकुवत किंवा पुन्हा वापरलेले पासवर्ड तपासता येतात. बॅकअप एकाच फाइलमध्ये निर्यात होतो, तुम्ही निवडलेल्या पासवर्डने एन्क्रिप्ट केलेला, आणि कोणत्याही उपकरणावर पुनर्संचयित करता येतो.\n\nPassKey विनामूल्य आणि मुक्तस्रोत आहे, Aditya Bhardwaj यांनी विकसित केलेले. संपूर्ण स्रोत कोड GitHub वर उपलब्ध आहे. + तुमची व्हॉल्ट तुमच्या उपकरणावरच राहते.\n\nआम्ही कधीही काय गोळा करत नाही: PassKey तुम्ही जतन केलेले पासवर्ड, वापरकर्तानावे, नोंदी किंवा इतर कोणतीही माहिती गोळा, प्रसारित किंवा साठवत नाही. ती फक्त तुमच्या फोनवरील एन्क्रिप्टेड डेटाबेसमध्ये असते. आम्ही ती वाचू शकत नाही आणि तुमच्यासाठी परत मिळवूही शकत नाही.\n\nॲप काय पाठवते: PassKey मध्ये Google Firebase Crashlytics, Analytics आणि Performance Monitoring, तसेच Google Play इन-ॲप अपडेट समाविष्ट आहेत. हे क्रॅश निदान, वापर घटना, कार्यक्षमता माहिती आणि जाहिरात आयडीसह उपकरण व स्थापना ओळखचिन्हे Google ला पाठवतात. त्यांना तुमच्या व्हॉल्टच्या मजकुरापर्यंत कधीही प्रवेश नसतो. या सेवांमुळे ॲपला इंटरनेट परवानगी लागते आणि त्यांची माहिती हाताळणी Google च्या गोपनीयता धोरणानुसार होते.\n\nबॅकअप: निर्यात करताना फाइल तुम्ही निवडलेल्या पासवर्डने एन्क्रिप्ट होते आणि तुम्ही सिस्टमला सांगाल तिथे साठवली जाते. त्यानंतर ती फाइल तुमची जबाबदारी आहे; ज्याच्याकडे फाइल आणि पासवर्ड दोन्ही असेल तो ती वाचू शकतो. आम्ही प्रत ठेवत नाही.\n\nउपकरण बॅकअप: PassKey ला Android क्लाउड बॅकअप आणि उपकरण-ते-उपकरण हस्तांतरणातून वगळले आहे, त्यामुळे तुमची व्हॉल्ट तुमच्या Google खात्यावर अपलोड होत नाही.\n\nबदल: हे धोरण अद्ययावत होऊ शकते; महत्त्वाचे बदल ॲपच्या रिलीज नोट्समध्ये नमूद केले जातील.\n\nप्रश्न: संपर्क yrkkh.cclub@gmail.com. + PassKey वापरून तुम्ही पुढील बाबींना सहमती देता.\n\n1. PassKey हे Aditya Bhardwaj यांनी विकसित केलेले विनामूल्य, मुक्तस्रोत सॉफ्टवेअर आहे.\n\n2. तुमची व्हॉल्ट फक्त तुमच्या उपकरणावर साठवली जाते. सर्व्हरवर प्रत नाही, त्यामुळे उपकरण हरवल्यास, ॲप काढून टाकल्यास किंवा बॅकअप पासवर्ड विसरल्यास डेटा परत मिळवण्याचा मार्ग नाही.\n\n3. बॅकअप ठेवणे ही तुमची जबाबदारी आहे. ते नियमितपणे निर्यात करा आणि बॅकअप पासवर्ड सुरक्षित, वेगळ्या ठिकाणी ठेवा.\n\n4. ॲप तुमच्या उपकरणाच्या बायोमेट्रिक किंवा स्क्रीन-लॉक सुरक्षेवर अवलंबून आहे. ते लॉक कमकुवत, काढलेले किंवा तडजोड झालेले असल्यास PassKey देत असलेले संरक्षण त्या प्रमाणात कमी होते.\n\n5. PassKey मध्ये गोपनीयता धोरणात वर्णन केलेल्या Google Firebase आणि Google Play सेवा समाविष्ट आहेत.\n\n6. हे ॲप्लिकेशन \"जसे आहे तसे\" पुरवले जाते, कोणत्याही स्पष्ट किंवा गर्भित हमीशिवाय, ज्यात व्यापारयोग्यता आणि विशिष्ट हेतूसाठी उपयुक्तता समाविष्ट आहे.\n\n7. कायद्याने परवानगी दिलेल्या मर्यादेत, ॲप्लिकेशनच्या वापरामुळे होणाऱ्या कोणत्याही नुकसानीस, डेटा हानीसह, विकसक जबाबदार नाही.\n\n8. ॲप्लिकेशन कधीही सुधारले किंवा बंद केले जाऊ शकते. \ No newline at end of file diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 1fcca50..2df2e3a 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -136,4 +136,7 @@ chave acesso + O PassKey guarda seus logins, senhas e outras notas privadas em um banco de dados criptografado no seu dispositivo.\n\nSeu cofre nunca sai do celular. Não há conta, sincronização nem servidor do PassKey: o aplicativo não faz requisições de rede próprias e suas entradas nunca são transmitidas para lugar algum.\n\nO banco de dados é criptografado com SQLCipher e desbloqueado com a biometria ou o bloqueio de tela do aparelho. O cofre volta a bloquear automaticamente quando você sai do app, capturas de tela são bloqueadas e senhas copiadas são marcadas como sensíveis e apagadas da área de transferência logo depois.\n\nVocê pode organizar entradas em categorias, reordená-las arrastando, gerar senhas fortes e verificar o cofre em busca de senhas fracas ou repetidas. Os backups são exportados como um único arquivo criptografado com uma senha escolhida por você e podem ser restaurados em qualquer dispositivo.\n\nO PassKey é gratuito e de código aberto, desenvolvido por Aditya Bhardwaj. O código-fonte completo está disponível no GitHub. + Seu cofre permanece no seu dispositivo.\n\nO que nunca coletamos: o PassKey não coleta, transmite nem armazena as senhas, nomes de usuário, notas ou quaisquer outras entradas que você salva. Elas existem apenas em um banco de dados criptografado no seu celular. Não podemos lê-las nem recuperá-las para você.\n\nO que o aplicativo envia: o PassKey inclui Google Firebase Crashlytics, Analytics e Performance Monitoring, além das atualizações no app do Google Play. Esses serviços enviam ao Google diagnósticos de falhas, eventos de uso, rastros de desempenho e identificadores de dispositivo e instalação, incluindo um identificador de publicidade. Eles nunca têm acesso ao conteúdo do seu cofre. Por causa desses serviços o app exige a permissão de internet, e o tratamento desses dados segue a política de privacidade do Google.\n\nBackups: ao exportar um backup, o arquivo é criptografado com uma senha escolhida por você e gravado onde você indicar ao sistema. Esse arquivo passa a ser responsabilidade sua; quem obtiver o arquivo e a senha poderá lê-lo. Não guardamos cópia.\n\nBackup do dispositivo: o PassKey está excluído do backup na nuvem do Android e da transferência entre dispositivos, então seu cofre não é enviado para sua conta Google.\n\nAlterações: esta política pode ser atualizada; mudanças relevantes serão indicadas nas notas de versão.\n\nDúvidas: escreva para yrkkh.cclub@gmail.com. + Ao usar o PassKey você concorda com o seguinte.\n\n1. O PassKey é software gratuito e de código aberto desenvolvido por Aditya Bhardwaj.\n\n2. Seu cofre é armazenado apenas no seu dispositivo. Não existe cópia em servidor e, portanto, não há como recuperar seus dados se você perder o aparelho, desinstalar o app ou esquecer uma senha de backup.\n\n3. Manter backups é responsabilidade sua. Exporte-os regularmente e guarde a senha do backup em local seguro e separado.\n\n4. O app depende da segurança biométrica ou do bloqueio de tela do seu aparelho. Se esse bloqueio for fraco, removido ou comprometido, a proteção oferecida pelo PassKey diminui na mesma medida.\n\n5. O PassKey inclui serviços do Google Firebase e do Google Play conforme descrito na Política de Privacidade.\n\n6. O aplicativo é fornecido \"no estado em que se encontra\", sem garantia de qualquer tipo, expressa ou implícita, incluindo comercialização e adequação a uma finalidade específica.\n\n7. Na medida permitida por lei, o desenvolvedor não se responsabiliza por danos decorrentes do uso do aplicativo, incluindo perda de dados.\n\n8. O aplicativo pode ser modificado ou descontinuado a qualquer momento. \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 2672789..7c68018 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -136,4 +136,7 @@ ключ вход + PassKey хранит ваши учётные данные, пароли и другие личные заметки в зашифрованной базе данных на вашем устройстве.\n\nВаше хранилище никогда не покидает телефон. Нет ни учётной записи, ни синхронизации, ни сервера PassKey: приложение не выполняет собственных сетевых запросов, и ваши записи никуда не передаются.\n\nБаза данных зашифрована с помощью SQLCipher и разблокируется биометрией или блокировкой экрана устройства. Хранилище автоматически блокируется, когда вы выходите из приложения, снимки экрана запрещены, а скопированные пароли помечаются как конфиденциальные и вскоре удаляются из буфера обмена.\n\nЗаписи можно распределять по категориям, менять их порядок перетаскиванием, генерировать надёжные пароли и проверять хранилище на слабые или повторяющиеся пароли. Резервные копии экспортируются одним файлом, зашифрованным выбранным вами паролем, и восстанавливаются на любом устройстве.\n\nPassKey — бесплатное приложение с открытым исходным кодом, разработанное Aditya Bhardwaj. Полный исходный код доступен на GitHub. + Ваше хранилище остаётся на вашем устройстве.\n\nЧто мы никогда не собираем: PassKey не собирает, не передаёт и не хранит пароли, имена пользователей, заметки и любые другие сохранённые вами записи. Они существуют только в зашифрованной базе данных на вашем телефоне. Мы не можем ни прочитать их, ни восстановить для вас.\n\nЧто отправляет приложение: PassKey включает Google Firebase Crashlytics, Analytics и Performance Monitoring, а также встроенные обновления Google Play. Эти сервисы отправляют в Google диагностику сбоев, события использования, данные о производительности, а также идентификаторы устройства и установки, включая рекламный идентификатор. Доступа к содержимому хранилища у них нет никогда. Из-за этих сервисов приложению требуется разрешение на доступ в интернет, а обработка этих данных регулируется политикой конфиденциальности Google.\n\nРезервные копии: при экспорте файл шифруется выбранным вами паролем и сохраняется туда, куда вы укажете системе. Далее этот файл — ваша ответственность; любой, у кого окажутся и файл, и пароль, сможет его прочитать. Мы копий не храним.\n\nРезервное копирование устройства: PassKey исключено из облачного резервного копирования Android и переноса между устройствами, поэтому хранилище не выгружается в ваш аккаунт Google.\n\nИзменения: политика может обновляться; существенные изменения отмечаются в примечаниях к выпуску.\n\nВопросы: пишите на yrkkh.cclub@gmail.com. + Используя PassKey, вы соглашаетесь со следующим.\n\n1. PassKey — бесплатное программное обеспечение с открытым исходным кодом, разработанное Aditya Bhardwaj.\n\n2. Ваше хранилище находится только на вашем устройстве. Серверной копии нет, а значит нет и способа восстановить данные, если вы потеряете устройство, удалите приложение или забудете пароль резервной копии.\n\n3. Резервные копии — ваша ответственность. Делайте их регулярно и храните пароль от копии в надёжном и отдельном месте.\n\n4. Приложение опирается на биометрию или блокировку экрана вашего устройства. Если эта защита слабая, отключена или скомпрометирована, защита со стороны PassKey снижается соответственно.\n\n5. PassKey включает сервисы Google Firebase и Google Play, как описано в политике конфиденциальности.\n\n6. Приложение предоставляется «как есть», без каких-либо гарантий, явных или подразумеваемых, включая пригодность для продажи и соответствие конкретной цели.\n\n7. В пределах, допускаемых законом, разработчик не несёт ответственности за ущерб, возникший в результате использования приложения, включая потерю данных.\n\n8. Приложение может быть изменено или прекращено в любое время. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index c9d0355..29657ee 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -134,4 +134,7 @@ சாவி உள்நுழைவு + PassKey உங்கள் உள்நுழைவுத் தகவல்கள், கடவுச்சொற்கள் மற்றும் பிற தனிப்பட்ட குறிப்புகளை உங்கள் சாதனத்தில் உள்ள மறையாக்கப்பட்ட தரவுத்தளத்தில் சேமிக்கிறது.\n\nஉங்கள் பெட்டகம் ஒருபோதும் தொலைபேசியை விட்டு வெளியே செல்வதில்லை. கணக்கு இல்லை, ஒத்திசைவு இல்லை, PassKey சேவையகமும் இல்லை: ஆப்ஸ் தானாக எந்த நெட்வொர்க் கோரிக்கையும் செய்வதில்லை, உங்கள் பதிவுகள் எங்கும் அனுப்பப்படுவதில்லை.\n\nதரவுத்தளம் SQLCipher மூலம் மறையாக்கப்பட்டு, உங்கள் சாதனத்தின் உயிரியல் அல்லது திரைப் பூட்டால் திறக்கப்படுகிறது. ஆப்ஸை விட்டு வெளியேறியதும் பெட்டகம் தானாகவே மீண்டும் பூட்டப்படும், திரைப்பிடிப்புகள் தடுக்கப்படுகின்றன, நகலெடுத்த கடவுச்சொற்கள் முக்கியமானவை எனக் குறிக்கப்பட்டு சிறிது நேரத்தில் இடைநிலைப் பலகையிலிருந்து அழிக்கப்படுகின்றன.\n\nபதிவுகளை வகைகளாக ஒழுங்குபடுத்தலாம், இழுத்து வரிசை மாற்றலாம், வலுவான கடவுச்சொற்களை உருவாக்கலாம், பலவீனமான அல்லது மீண்டும் பயன்படுத்திய கடவுச்சொற்களைச் சரிபார்க்கலாம். காப்புப்பிரதிகள் நீங்கள் தேர்ந்தெடுத்த கடவுச்சொல்லால் மறையாக்கப்பட்ட ஒற்றைக் கோப்பாக ஏற்றுமதி செய்யப்பட்டு எந்தச் சாதனத்திலும் மீட்டெடுக்கப்படலாம்.\n\nPassKey இலவசமானது, திறந்த மூலமானது, Aditya Bhardwaj உருவாக்கியது. முழு மூலக் குறியீடும் GitHub இல் கிடைக்கிறது. + உங்கள் பெட்டகம் உங்கள் சாதனத்திலேயே இருக்கும்.\n\nநாங்கள் ஒருபோதும் சேகரிக்காதவை: நீங்கள் சேமிக்கும் கடவுச்சொற்கள், பயனர்பெயர்கள், குறிப்புகள் அல்லது பிற எந்தப் பதிவையும் PassKey சேகரிக்கவோ, அனுப்பவோ, சேமிக்கவோ இல்லை. அவை உங்கள் தொலைபேசியில் உள்ள மறையாக்கப்பட்ட தரவுத்தளத்தில் மட்டுமே இருக்கும். அவற்றை எங்களால் படிக்கவும் முடியாது, உங்களுக்காக மீட்டெடுக்கவும் முடியாது.\n\nஆப்ஸ் அனுப்புபவை: PassKey இல் Google Firebase Crashlytics, Analytics, Performance Monitoring மற்றும் Google Play ஆப்ஸ்-உள் புதுப்பிப்புகள் உள்ளன. இவை செயலிழப்பு நோயறிதல், பயன்பாட்டு நிகழ்வுகள், செயல்திறன் தரவு, விளம்பர ID உள்ளிட்ட சாதன மற்றும் நிறுவல் அடையாளங்காட்டிகளை Google க்கு அனுப்புகின்றன. உங்கள் பெட்டக உள்ளடக்கத்திற்கு இவற்றுக்கு ஒருபோதும் அணுகல் இல்லை. இச்சேவைகளால் ஆப்ஸுக்கு இணைய அனுமதி தேவை, அவற்றின் தரவு கையாளுதல் Google இன் தனியுரிமைக் கொள்கையின் கீழ் வருகிறது.\n\nகாப்புப்பிரதிகள்: ஏற்றுமதி செய்யும்போது கோப்பு நீங்கள் தேர்ந்தெடுத்த கடவுச்சொல்லால் மறையாக்கப்பட்டு, நீங்கள் கணினியிடம் குறிப்பிட்ட இடத்தில் சேமிக்கப்படும். அதன் பிறகு அக்கோப்பு உங்கள் பொறுப்பு; கோப்பையும் கடவுச்சொல்லையும் பெறுபவர் அதைப் படிக்க முடியும். நாங்கள் நகல் வைத்திருப்பதில்லை.\n\nசாதனக் காப்புப்பிரதி: PassKey Android கிளவுட் காப்புப்பிரதி மற்றும் சாதனங்களுக்கிடையேயான இடமாற்றத்திலிருந்து விலக்கப்பட்டுள்ளது, எனவே உங்கள் பெட்டகம் உங்கள் Google கணக்கிற்குப் பதிவேற்றப்படுவதில்லை.\n\nமாற்றங்கள்: இக்கொள்கை புதுப்பிக்கப்படலாம்; முக்கிய மாற்றங்கள் ஆப்ஸின் வெளியீட்டுக் குறிப்புகளில் தெரிவிக்கப்படும்.\n\nகேள்விகள்: தொடர்பு yrkkh.cclub@gmail.com. + PassKey ஐப் பயன்படுத்துவதன் மூலம் பின்வருவனவற்றை ஏற்கிறீர்கள்.\n\n1. PassKey என்பது Aditya Bhardwaj உருவாக்கிய இலவச, திறந்த மூல மென்பொருள்.\n\n2. உங்கள் பெட்டகம் உங்கள் சாதனத்தில் மட்டுமே சேமிக்கப்படுகிறது. சேவையகத்தில் நகல் இல்லை, எனவே சாதனத்தை இழந்தால், ஆப்ஸை நீக்கினால் அல்லது காப்புப்பிரதி கடவுச்சொல்லை மறந்தால் தரவை மீட்க வழி இல்லை.\n\n3. காப்புப்பிரதிகளை வைத்திருப்பது உங்கள் பொறுப்பு. அவற்றை வழக்கமாக ஏற்றுமதி செய்து, காப்புப்பிரதி கடவுச்சொல்லைப் பாதுகாப்பான, தனித்த இடத்தில் வைக்கவும்.\n\n4. ஆப்ஸ் உங்கள் சாதனத்தின் உயிரியல் அல்லது திரைப் பூட்டுப் பாதுகாப்பை நம்பியுள்ளது. அப்பூட்டு பலவீனமாக, அகற்றப்பட்டதாக அல்லது சமரசம் செய்யப்பட்டதாக இருந்தால், PassKey அளிக்கும் பாதுகாப்பும் அதற்கேற்ப குறையும்.\n\n5. PassKey இல் தனியுரிமைக் கொள்கையில் விவரிக்கப்பட்ட Google Firebase மற்றும் Google Play சேவைகள் உள்ளன.\n\n6. இப்பயன்பாடு \"உள்ளபடி\" வழங்கப்படுகிறது, வணிகத்தகுதி மற்றும் குறிப்பிட்ட நோக்கத்திற்கான உகந்ததன்மை உட்பட, வெளிப்படையான அல்லது மறைமுகமான எந்த உத்தரவாதமும் இன்றி.\n\n7. சட்டம் அனுமதிக்கும் அளவில், பயன்பாட்டின் பயன்பாட்டால் ஏற்படும் எந்த சேதத்திற்கும், தரவு இழப்பு உட்பட, உருவாக்குநர் பொறுப்பல்ல.\n\n8. பயன்பாடு எந்த நேரத்திலும் மாற்றப்படலாம் அல்லது நிறுத்தப்படலாம். diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index c1ca3fa..6a8862b 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -134,5 +134,8 @@ కీ లాగిన్ + PassKey మీ లాగిన్‌లు, పాస్‌వర్డ్‌లు మరియు ఇతర వ్యక్తిగత గమనికలను మీ పరికరంలోని ఎన్‌క్రిప్ట్ చేసిన డేటాబేస్‌లో నిల్వ చేస్తుంది.\n\nమీ వాల్ట్ ఎప్పుడూ మీ ఫోన్‌ను వదిలి వెళ్లదు. ఖాతా లేదు, సింక్ లేదు, PassKey సర్వర్ కూడా లేదు: యాప్ తనంతట తానుగా ఎలాంటి నెట్‌వర్క్ అభ్యర్థనలూ చేయదు, మీ ఎంట్రీలు ఎక్కడికీ పంపబడవు.\n\nడేటాబేస్ SQLCipher తో ఎన్‌క్రిప్ట్ చేయబడి, మీ పరికరం బయోమెట్రిక్ లేదా స్క్రీన్ లాక్‌తో తెరుచుకుంటుంది. యాప్ నుండి బయటకు వెళ్లగానే వాల్ట్ స్వయంచాలకంగా మళ్లీ లాక్ అవుతుంది, స్క్రీన్‌షాట్‌లు నిరోధించబడ్డాయి, కాపీ చేసిన పాస్‌వర్డ్‌లు సున్నితమైనవిగా గుర్తించబడి కొద్దిసేపటిలో క్లిప్‌బోర్డ్ నుండి తొలగించబడతాయి.\n\nఎంట్రీలను వర్గాలుగా అమర్చవచ్చు, లాగి క్రమం మార్చవచ్చు, బలమైన పాస్‌వర్డ్‌లు సృష్టించవచ్చు, బలహీనమైన లేదా పునరావృత పాస్‌వర్డ్‌లను తనిఖీ చేయవచ్చు. బ్యాకప్‌లు మీరు ఎంచుకున్న పాస్‌వర్డ్‌తో ఎన్‌క్రిప్ట్ చేసిన ఒకే ఫైల్‌గా ఎగుమతి అవుతాయి, ఏ పరికరంలోనైనా పునరుద్ధరించవచ్చు.\n\nPassKey ఉచితం, ఓపెన్ సోర్స్, Aditya Bhardwaj అభివృద్ధి చేశారు. పూర్తి సోర్స్ కోడ్ GitHub లో అందుబాటులో ఉంది. + మీ వాల్ట్ మీ పరికరంలోనే ఉంటుంది.\n\nమేము ఎప్పుడూ సేకరించనివి: మీరు సేవ్ చేసిన పాస్‌వర్డ్‌లు, వినియోగదారు పేర్లు, గమనికలు లేదా ఇతర ఏ ఎంట్రీలనూ PassKey సేకరించదు, పంపదు, నిల్వ చేయదు. అవి మీ ఫోన్‌లోని ఎన్‌క్రిప్ట్ చేసిన డేటాబేస్‌లో మాత్రమే ఉంటాయి. మేము వాటిని చదవలేము, మీ కోసం తిరిగి పొందలేము.\n\nయాప్ పంపేవి: PassKey లో Google Firebase Crashlytics, Analytics, Performance Monitoring మరియు Google Play ఇన్-యాప్ అప్‌డేట్‌లు ఉన్నాయి. ఇవి క్రాష్ డయాగ్నస్టిక్స్, వినియోగ ఈవెంట్‌లు, పనితీరు డేటా, ప్రకటన IDతో సహా పరికర మరియు ఇన్‌స్టాలేషన్ గుర్తింపులను Google కు పంపుతాయి. మీ వాల్ట్ కంటెంట్‌కు వీటికి ఎప్పుడూ ప్రాప్యత ఉండదు. ఈ సేవల కారణంగా యాప్‌కు ఇంటర్నెట్ అనుమతి అవసరం, వాటి డేటా నిర్వహణ Google గోప్యతా విధానం ప్రకారం జరుగుతుంది.\n\nబ్యాకప్‌లు: ఎగుమతి చేసినప్పుడు ఫైల్ మీరు ఎంచుకున్న పాస్‌వర్డ్‌తో ఎన్‌క్రిప్ట్ అవుతుంది, మీరు సిస్టమ్‌కు చెప్పిన చోట నిల్వ అవుతుంది. ఆ తర్వాత ఆ ఫైల్ మీ బాధ్యత; ఫైల్ మరియు పాస్‌వర్డ్ రెండూ పొందిన ఎవరైనా దాన్ని చదవగలరు. మేము ఎలాంటి కాపీ ఉంచుకోము.\n\nపరికర బ్యాకప్: PassKey Android క్లౌడ్ బ్యాకప్ మరియు పరికరాల మధ్య బదిలీ నుండి మినహాయించబడింది, కాబట్టి మీ వాల్ట్ మీ Google ఖాతాకు అప్‌లోడ్ కాదు.\n\nమార్పులు: ఈ విధానం నవీకరించబడవచ్చు; ముఖ్యమైన మార్పులు యాప్ విడుదల గమనికలలో తెలియజేయబడతాయి.\n\nప్రశ్నలు: సంప్రదించండి yrkkh.cclub@gmail.com. + PassKey ను ఉపయోగించడం ద్వారా మీరు కింది వాటికి అంగీకరిస్తున్నారు.\n\n1. PassKey అనేది Aditya Bhardwaj అభివృద్ధి చేసిన ఉచిత, ఓపెన్-సోర్స్ సాఫ్ట్‌వేర్.\n\n2. మీ వాల్ట్ మీ పరికరంలో మాత్రమే నిల్వ చేయబడుతుంది. సర్వర్‌లో కాపీ లేదు, కాబట్టి పరికరం పోగొట్టుకున్నా, యాప్ అన్‌ఇన్‌స్టాల్ చేసినా, బ్యాకప్ పాస్‌వర్డ్ మరచిపోయినా డేటాను తిరిగి పొందే మార్గం లేదు.\n\n3. బ్యాకప్‌లు ఉంచుకోవడం మీ బాధ్యత. వాటిని క్రమం తప్పకుండా ఎగుమతి చేయండి, బ్యాకప్ పాస్‌వర్డ్‌ను సురక్షితమైన, వేరే చోట ఉంచండి.\n\n4. యాప్ మీ పరికరం బయోమెట్రిక్ లేదా స్క్రీన్-లాక్ భద్రతపై ఆధారపడి ఉంటుంది. ఆ లాక్ బలహీనంగా, తొలగించబడి లేదా రాజీపడి ఉంటే, PassKey అందించే రక్షణ కూడా అదే మేరకు తగ్గుతుంది.\n\n5. PassKey లో గోప్యతా విధానంలో వివరించిన Google Firebase మరియు Google Play సేవలు ఉన్నాయి.\n\n6. ఈ అప్లికేషన్ \"ఉన్నది ఉన్నట్లుగా\" అందించబడుతుంది, వర్తకయోగ్యత మరియు నిర్దిష్ట ప్రయోజనానికి అనుకూలత సహా, ఎలాంటి స్పష్టమైన లేదా అంతర్గత హామీ లేకుండా.\n\n7. చట్టం అనుమతించిన మేరకు, అప్లికేషన్ వినియోగం వల్ల కలిగే ఏ నష్టానికీ, డేటా నష్టంతో సహా, డెవలపర్ బాధ్యత వహించరు.\n\n8. అప్లికేషన్ ఎప్పుడైనా మార్చబడవచ్చు లేదా నిలిపివేయబడవచ్చు. diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index c8e88d2..0a28f55 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -134,4 +134,7 @@ 秘密 登录 + PassKey 将你的登录信息、密码和其他私密备注保存在设备上的加密数据库中。\n\n你的密码库永远不会离开手机。没有账号、没有同步、也没有 PassKey 服务器:应用本身不发起任何网络请求,你的条目不会被传输到任何地方。\n\n数据库使用 SQLCipher 加密,通过设备的生物识别或屏幕锁解锁。离开应用后密码库会自动重新锁定,截图被禁用,复制的密码会被标记为敏感内容并在稍后从剪贴板清除。\n\n你可以按类别整理条目、拖动排序、生成高强度密码,并检查密码库中是否存在弱密码或重复使用的密码。备份会导出为单个文件,使用你自己设定的密码加密,可在任意设备上恢复。\n\nPassKey 由 Aditya Bhardwaj 开发,免费且开源,完整源代码可在 GitHub 上获取。 + 你的密码库保留在你的设备上。\n\n我们从不收集的内容:PassKey 不会收集、传输或存储你保存的密码、用户名、备注或任何其他条目。它们只存在于你手机上的加密数据库中。我们既无法读取,也无法替你恢复。\n\n应用确实会发送的内容:PassKey 集成了 Google Firebase Crashlytics、Analytics 和 Performance Monitoring,以及 Google Play 应用内更新。这些服务会向 Google 发送崩溃诊断、使用事件、性能数据,以及包含广告 ID 在内的设备和安装标识符。它们始终无法访问你的密码库内容。由于这些服务,应用需要网络权限,其数据处理受 Google 隐私政策约束。\n\n备份:导出备份时,文件会使用你设定的密码加密,并保存到你指定的位置。此后该文件由你自行保管;同时获得文件和密码的人即可读取其内容。我们不保留任何副本。\n\n设备备份:PassKey 已排除在 Android 云备份和设备间转移之外,因此你的密码库不会上传到你的 Google 账号。\n\n变更:本政策可能更新,重大变更会在应用的版本说明中注明。\n\n如有疑问,请联系 yrkkh.cclub@gmail.com。 + 使用 PassKey 即表示你同意以下条款。\n\n1. PassKey 是由 Aditya Bhardwaj 开发的免费开源软件。\n\n2. 你的密码库仅存储在你的设备上。不存在服务器端副本,因此如果你丢失设备、卸载应用或忘记备份密码,将无法恢复数据。\n\n3. 保留备份是你的责任。请定期导出,并将备份密码保存在安全且独立的地方。\n\n4. 本应用依赖你设备的生物识别或屏幕锁安全性。若该锁定较弱、被移除或已被攻破,PassKey 提供的保护也会相应降低。\n\n5. PassKey 包含隐私政策中所述的 Google Firebase 和 Google Play 服务。\n\n6. 本应用按\"现状\"提供,不作任何明示或暗示的保证,包括适销性和特定用途适用性。\n\n7. 在法律允许的范围内,开发者不对因使用本应用而产生的任何损害负责,包括数据丢失。\n\n8. 本应用可能随时被修改或停止提供。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c283603..04b6399 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,16 +24,45 @@ Don’t compromise your passwords by typing them in public, let PassKey autofill those and keep your credentials secure. - - Welcome to Passkey: Password Manager, the ultimate solution for managing all your passwords and private information securely!\n\nAre you tired of remembering multiple passwords or annoyed of forgetting them? Passkey: Password Manager helps you store all your logins, passwords, and other private information safe and secure in an encrypted database.\n\nPasskey: Password Manager is an open-source application developed by Aditya Bhardwaj. It is designed to provide a secure way to manage all your passwords without worrying about data leaks. With Passkey: Password Manager, you can store your passwords locally on your device, and the application does not require internet permissions, making it even more secure.\n\nPasskey: Password Manager features an encrypted database to store your passwords, and biometric authentication is used to access your information. The application supports dark mode, and screenshot blocking features to ensure that your information is not accessible by unauthorized persons.\n\nThe application uses Drag & Drop and Swipe to Delete features, providing a user-friendly interface to manage all your passwords easily. The application also has a Separation of Content feature, which allows you to categorize your passwords into different sections for easy management.\n\nPasskey: Password Manager uses LiveData and MVVM architecture to ensure that the application is scalable, maintainable, and easy to use. SQL Cipher is used for Room DB Backup Encryption and Decryption, and Dagger-Hilt is used for dependency injection.\n\nIn conclusion, Passkey: Password Manager is a must-have application for everyone who wants to manage their passwords securely. With its features and user-friendly interface, you can keep all your passwords and private information safe and secure. Download Passkey: Password Manager now, and never worry about forgetting your passwords again! - At Passkey: Password Manager, we are committed to protecting your privacy. This privacy policy explains how we collect, use, and share your personal information when you use our application.\n\nInformation we collect: We do not collect any of your passwords or private information that you store in the Passkey: Password Manager application. Your information is stored locally on your phone and encrypted for added security.\n\nHow we use your information: We use your information to store your passwords and private information securely in an encrypted database. We do not share your information with any third-party.\n\nHow we protect your information: We use encrypted database storage to protect your information, and biometric authentication is used to access your information.\n\nHow we share your information: We do not share your information with any third-party.\n\nChanges to our privacy policy: We reserve the right to modify this privacy policy at any time without prior notice.\n\nIf you have any questions or concerns about our terms and conditions or privacy policy, please contact us at yrkkh.cclub@gmail.com\n\nWe hope you enjoy using Passkey: Password Manager to store your passwords and private information securely. - Welcome to Passkey: Password Manager! By downloading and using this application, you agree to comply with and be bound by the following terms and conditions:\n\n1. Passkey: Password Manager is an application developed by Aditya Bhardwaj, and the intellectual property rights are owned by Aditya Bhardwaj.\n\n2. The Passkey: Password Manager application is designed to store your passwords and private information securely in an encrypted database.\n\n3. The Passkey: Password Manager application does not store your credentials on servers, so your passwords and private information are in your hands.\n\n4. The Passkey: Password Manager application does not require any internet permission, and it is an open-source application.\n\n5. The Passkey: Password Manager application uses encrypted database storage to store your information, and biometric authentication is used to access your information.\n\n6. The Passkey: Password Manager application is provided "as is" without any warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and fitness for a particular purpose.\n\n7. Aditya Bhardwaj is not liable for any damages arising from the use of the Passkey: Password Manager application, including but not limited to direct, indirect, incidental, or consequential damages.\n\n8. By downloading and using the Passkey: Password Manager application, you agree to indemnify and hold harmless Aditya Bhardwaj, its officers, employees, agents, and affiliates from any and all claims, damages, losses, liabilities, and expenses arising from your use of the application.\n\n9. Aditya Bhardwaj reserves the right to modify, suspend, or discontinue the Passkey: Password Manager application at any time without prior notice. + PassKey stores your logins, passwords and other private notes in an encrypted database on your device. + +Your vault never leaves your phone. There is no account, no sync and no PassKey server: the app makes no network requests of its own, and your entries are never transmitted anywhere. + +The database is encrypted with SQLCipher and unlocked with your device biometrics or screen lock. The vault re-locks automatically when you leave the app, screenshots are blocked, and copied passwords are marked sensitive and cleared from the clipboard shortly after. + +You can organise entries into categories, reorder them by dragging, generate strong passwords, and check your vault for weak or reused ones. Backups are exported as a single file encrypted with a password you choose, and can be restored on any device. + +PassKey is free and open source, developed by Aditya Bhardwaj. The full source is available on GitHub. + Your vault stays on your device. + +What we never collect: PassKey does not collect, transmit or store the passwords, usernames, notes or any other entries you save. They live only in an encrypted database on your phone. We cannot read them, and we cannot recover them for you. + +What the app does send: PassKey includes Google Firebase Crashlytics, Analytics and Performance Monitoring, and Google Play in-app updates. These send crash diagnostics, app usage events, performance traces, and device and installation identifiers including an advertising ID to Google. They never have access to your vault contents. Because of these services the app requires the internet permission, and their data handling is governed by Google\'s privacy policy. + +Backups: when you export a backup, the file is encrypted with a password you choose and written wherever you tell the system to put it. That file is then yours to manage; anyone who obtains both the file and its password can read it. We keep no copy. + +Device backup: PassKey is excluded from Android cloud backup and device-to-device transfer, so your vault is not uploaded to your Google account. + +Changes: this policy may be updated; material changes will be noted in the app\'s release notes. + +Questions: contact yrkkh.cclub@gmail.com. + By using PassKey you agree to the following. + +1. PassKey is free, open-source software developed by Aditya Bhardwaj. + +2. Your vault is stored only on your device. There is no server-side copy, and therefore no way for anyone to recover your data if you lose your device, uninstall the app, or forget a backup password. + +3. Keeping backups is your responsibility. Export them regularly and store the backup password somewhere safe and separate. + +4. The app relies on your device\'s biometric or screen-lock security. If your device lock is weak, removed or compromised, the protection PassKey provides is reduced accordingly. + +5. PassKey includes Google Firebase and Google Play services as described in the Privacy Policy. + +6. The application is provided \"as is\", without warranty of any kind, express or implied, including merchantability and fitness for a particular purpose. + +7. To the extent permitted by law, the developer is not liable for any damages arising from use of the application, including loss of data. + +8. The application may be modified or discontinued at any time.