Compare commits
2 Commits
15e075b293
...
f50226f88f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f50226f88f | ||
|
|
61cb32a5f5 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -10,4 +10,7 @@
|
||||
local.properties
|
||||
.claude
|
||||
CLAUDE.md
|
||||
keystore.properties
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
|
||||
20
LICENSE
Normal file
20
LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
Password Zebra
|
||||
Copyright (C) 2024 Pavel Bubenicek
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
---
|
||||
|
||||
[Append the full text of the GNU General Public License v3 below.
|
||||
Download it from: https://www.gnu.org/licenses/gpl-3.0.txt]
|
||||
@ -1,8 +1,15 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
val keystoreProps = Properties().also { props ->
|
||||
val f = rootProject.file("keystore.properties")
|
||||
if (f.exists()) props.load(f.inputStream())
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "cz.bugsy.passwordzebra"
|
||||
compileSdk = 35
|
||||
@ -17,9 +24,19 @@ android {
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
storeFile = file(keystoreProps["storeFile"] as String)
|
||||
storePassword = keystoreProps["storePassword"] as String
|
||||
keyAlias = keystoreProps["keyAlias"] as String
|
||||
keyPassword = keystoreProps["keyPassword"] as String
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
@ -35,6 +52,7 @@ android {
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.10"
|
||||
|
||||
@ -19,10 +19,7 @@ data class DeterministicState(
|
||||
val masterPassword: CharArray = CharArray(0),
|
||||
val masterPasswordVisible: Boolean = false,
|
||||
val generatedPassword: String = "",
|
||||
val isLocked: Boolean = false,
|
||||
val clipboardTimerSeconds: Int = 0,
|
||||
val biometricEnabled: Boolean = false,
|
||||
val lockTimeoutMinutes: Int = 5,
|
||||
val showRotateWarning: Boolean = false,
|
||||
val serviceHistory: List<String> = emptyList(),
|
||||
) {
|
||||
@ -34,10 +31,7 @@ data class DeterministicState(
|
||||
masterPassword.contentEquals(other.masterPassword) &&
|
||||
masterPasswordVisible == other.masterPasswordVisible &&
|
||||
generatedPassword == other.generatedPassword &&
|
||||
isLocked == other.isLocked &&
|
||||
clipboardTimerSeconds == other.clipboardTimerSeconds &&
|
||||
biometricEnabled == other.biometricEnabled &&
|
||||
lockTimeoutMinutes == other.lockTimeoutMinutes &&
|
||||
showRotateWarning == other.showRotateWarning &&
|
||||
serviceHistory == other.serviceHistory
|
||||
}
|
||||
@ -47,10 +41,7 @@ data class DeterministicState(
|
||||
result = 31 * result + masterPassword.contentHashCode()
|
||||
result = 31 * result + masterPasswordVisible.hashCode()
|
||||
result = 31 * result + generatedPassword.hashCode()
|
||||
result = 31 * result + isLocked.hashCode()
|
||||
result = 31 * result + clipboardTimerSeconds
|
||||
result = 31 * result + biometricEnabled.hashCode()
|
||||
result = 31 * result + lockTimeoutMinutes
|
||||
result = 31 * result + showRotateWarning.hashCode()
|
||||
result = 31 * result + serviceHistory.hashCode()
|
||||
return result
|
||||
@ -66,18 +57,10 @@ class DeterministicViewModel(app: Application) : AndroidViewModel(app) {
|
||||
private val _state = MutableStateFlow(DeterministicState())
|
||||
val state: StateFlow<DeterministicState> = _state.asStateFlow()
|
||||
|
||||
private val settingsPrefs = app.getSharedPreferences("det_settings", Context.MODE_PRIVATE)
|
||||
|
||||
private var clipboardClearJob: Job? = null
|
||||
|
||||
init {
|
||||
_state.update {
|
||||
it.copy(
|
||||
biometricEnabled = settingsPrefs.getBoolean("biometric_enabled", false),
|
||||
lockTimeoutMinutes = settingsPrefs.getInt("lock_timeout_minutes", 5),
|
||||
serviceHistory = historyRepository.getNames(),
|
||||
)
|
||||
}
|
||||
_state.update { it.copy(serviceHistory = historyRepository.getNames()) }
|
||||
}
|
||||
|
||||
fun updateServiceName(name: String) = _state.update { it.copy(serviceName = name.lowercase()) }
|
||||
@ -143,27 +126,6 @@ class DeterministicViewModel(app: Application) : AndroidViewModel(app) {
|
||||
}
|
||||
}
|
||||
|
||||
fun lock() = _state.update { it.copy(isLocked = true) }
|
||||
|
||||
fun unlock() = _state.update { it.copy(isLocked = false) }
|
||||
|
||||
fun onAppForeground(lastBackgroundTimeMs: Long) {
|
||||
val timeoutMs = _state.value.lockTimeoutMinutes * 60 * 1000L
|
||||
if (timeoutMs > 0 && System.currentTimeMillis() - lastBackgroundTimeMs >= timeoutMs) {
|
||||
lock()
|
||||
}
|
||||
}
|
||||
|
||||
fun setBiometricEnabled(enabled: Boolean) {
|
||||
settingsPrefs.edit().putBoolean("biometric_enabled", enabled).apply()
|
||||
_state.update { it.copy(biometricEnabled = enabled) }
|
||||
}
|
||||
|
||||
fun setLockTimeoutMinutes(minutes: Int) {
|
||||
settingsPrefs.edit().putInt("lock_timeout_minutes", minutes).apply()
|
||||
_state.update { it.copy(lockTimeoutMinutes = minutes) }
|
||||
}
|
||||
|
||||
fun getDeviceSecret(): ByteArray = deviceSecretManager.exportSecret()
|
||||
|
||||
fun importDeviceSecret(secret: ByteArray) = deviceSecretManager.importSecret(secret)
|
||||
|
||||
@ -180,7 +180,6 @@ fun AppNavigation() {
|
||||
popExitTransition = { slideOutHorizontally(tween(300)) { it } },
|
||||
) {
|
||||
SettingsScreen(
|
||||
viewModel = detViewModel,
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToExportImport = { navController.navigate(ROUTE_EXPORT_IMPORT) },
|
||||
)
|
||||
|
||||
@ -81,10 +81,6 @@ fun DeterministicScreen(
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
BackHandler { (context as? android.app.Activity)?.finish() }
|
||||
if (state.isLocked) {
|
||||
LockScreen(viewModel = viewModel)
|
||||
return
|
||||
}
|
||||
|
||||
var showRotateConfirmDialog by remember { mutableStateOf(false) }
|
||||
val masterPasswordFocus = remember { FocusRequester() }
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
package cz.bugsy.passwordzebra.ui.screens
|
||||
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import cz.bugsy.passwordzebra.R
|
||||
import cz.bugsy.passwordzebra.deterministic.DeterministicViewModel
|
||||
|
||||
@Composable
|
||||
fun LockScreen(viewModel: DeterministicViewModel) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
fun launchBiometric() {
|
||||
val activity = context as? FragmentActivity ?: return
|
||||
val executor = ContextCompat.getMainExecutor(context)
|
||||
val prompt = BiometricPrompt(
|
||||
activity,
|
||||
executor,
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
||||
viewModel.unlock()
|
||||
}
|
||||
},
|
||||
)
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(context.getString(R.string.det_biometric_prompt_title))
|
||||
.setSubtitle(context.getString(R.string.det_biometric_prompt_subtitle))
|
||||
.setNegativeButtonText(context.getString(R.string.cancel))
|
||||
.build()
|
||||
prompt.authenticate(promptInfo)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (state.biometricEnabled) {
|
||||
launchBiometric()
|
||||
}
|
||||
}
|
||||
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(64.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.det_locked_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.det_locked_body),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
if (state.biometricEnabled) {
|
||||
Button(onClick = { launchBiometric() }) {
|
||||
Text(stringResource(R.string.det_unlock_biometric))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,6 @@ import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -15,34 +14,25 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
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.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import cz.bugsy.passwordzebra.BuildConfig
|
||||
import cz.bugsy.passwordzebra.R
|
||||
import cz.bugsy.passwordzebra.deterministic.DeterministicViewModel
|
||||
|
||||
private val AUTH = BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
||||
@ -50,20 +40,11 @@ private val AUTH = BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
viewModel: DeterministicViewModel,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToExportImport: () -> Unit,
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val context = LocalContext.current
|
||||
|
||||
val timeoutOptions = listOf(0, 1, 5, 15)
|
||||
var timeoutDropdownExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
fun timeoutLabel(minutes: Int): String = when (minutes) {
|
||||
0 -> context.getString(R.string.det_timeout_off)
|
||||
else -> context.getString(R.string.det_timeout_minutes, minutes)
|
||||
}
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
fun launchExportImportAuth() {
|
||||
val activity = context as? FragmentActivity ?: return
|
||||
@ -106,77 +87,6 @@ fun SettingsScreen(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// Biometric toggle
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.det_biometric_toggle),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.det_biometric_toggle_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = state.biometricEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
if (enabled) {
|
||||
val bio = BiometricManager.from(context)
|
||||
if (bio.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) ==
|
||||
BiometricManager.BIOMETRIC_SUCCESS
|
||||
) {
|
||||
viewModel.setBiometricEnabled(true)
|
||||
}
|
||||
} else {
|
||||
viewModel.setBiometricEnabled(false)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Lock timeout
|
||||
Text(
|
||||
text = stringResource(R.string.det_lock_timeout),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = timeoutDropdownExpanded,
|
||||
onExpandedChange = { timeoutDropdownExpanded = it },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = timeoutLabel(state.lockTimeoutMinutes),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = timeoutDropdownExpanded) },
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = timeoutDropdownExpanded,
|
||||
onDismissRequest = { timeoutDropdownExpanded = false },
|
||||
) {
|
||||
timeoutOptions.forEach { minutes ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(timeoutLabel(minutes)) },
|
||||
onClick = {
|
||||
viewModel.setLockTimeoutMinutes(minutes)
|
||||
timeoutDropdownExpanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Argon2 info
|
||||
Text(
|
||||
text = stringResource(R.string.det_argon2_info_title),
|
||||
@ -198,6 +108,71 @@ fun SettingsScreen(
|
||||
Text(stringResource(R.string.det_export_import_title))
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Privacy
|
||||
Text(
|
||||
text = stringResource(R.string.privacy_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.privacy_body),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// About / License
|
||||
Text(
|
||||
text = stringResource(R.string.about_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.about_version, BuildConfig.VERSION_NAME),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.about_license_title),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.about_license_body),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.about_oss_title),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.about_oss_body),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Contributing
|
||||
Text(
|
||||
text = stringResource(R.string.about_contributing_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
TextButton(
|
||||
onClick = { uriHandler.openUri("https://git.bugsy.cz/beval/password_zebra") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.about_source_code))
|
||||
}
|
||||
TextButton(
|
||||
onClick = { uriHandler.openUri("https://git.bugsy.cz/beval/password_zebra/issues") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.about_issues))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,20 +58,23 @@
|
||||
|
||||
<!-- Nastavení -->
|
||||
<string name="det_settings_title">Nastavení</string>
|
||||
<string name="det_biometric_toggle">Biometrické odemčení</string>
|
||||
<string name="det_biometric_toggle_desc">Odemkněte trezor otiskem prstu nebo tváří</string>
|
||||
<string name="det_lock_timeout">Automatické zamčení</string>
|
||||
<string name="det_timeout_off">Vypnuto</string>
|
||||
<string name="det_timeout_minutes">%d minut</string>
|
||||
<string name="det_argon2_info_title">Parametry Argon2id</string>
|
||||
<string name="det_argon2_info_body">Paměť: 64 MB · Iterace: 3 · Paralelismus: 1 · Výstup: 64 bajtů</string>
|
||||
|
||||
<!-- Zamčená obrazovka -->
|
||||
<string name="det_locked_title">Trezor zamčen</string>
|
||||
<string name="det_locked_body">Trezor byl zamčen z důvodu nečinnosti.</string>
|
||||
<string name="det_unlock_biometric">Odemknout biometrikou</string>
|
||||
<string name="det_biometric_prompt_title">Odemknout trezor</string>
|
||||
<string name="det_biometric_prompt_subtitle">Ověřte se pro přístup do trezoru</string>
|
||||
<string name="det_export_locked_body">Ověřte se pro zobrazení QR zálohy vašeho device secret.</string>
|
||||
<string name="det_import_biometric_subtitle">Potvrzení nahrazení device secret</string>
|
||||
|
||||
<!-- Ochrana soukromí -->
|
||||
<string name="privacy_title">Ochrana soukromí</string>
|
||||
<string name="privacy_body">Password Zebra funguje zcela offline. Žádná data nejsou sbírána, přenášena ani ukládána mimo vaše zařízení. Přístup ke kameře slouží výhradně k lokálnímu skenování QR kódů. Biometrické ověření je plně zajišťováno operačním systémem Android; tato aplikace nemá přístup k žádným biometrickým datům.</string>
|
||||
|
||||
<!-- O aplikaci / Licence -->
|
||||
<string name="about_title">O aplikaci</string>
|
||||
<string name="about_version">Verze %s</string>
|
||||
<string name="about_license_title">Licence</string>
|
||||
<string name="about_license_body">Tato aplikace je svobodný software šířený pod licencí GNU General Public License verze 3 (GPL-3.0-or-later). Můžete ji šířit a/nebo upravovat za podmínek GPL vydané Free Software Foundation, verze 3 nebo (dle vaší volby) jakékoli novější verze.\n\nTento program je šířen BEZ ZÁRUKY; bez jakékoli záruky prodejnosti nebo vhodnosti pro konkrétní účel.</string>
|
||||
<string name="about_oss_title">Open-source komponenty</string>
|
||||
<string name="about_oss_body">Bouncy Castle (MIT) · ZXing / ZXing Android Embedded (Apache 2.0) · Jetpack Compose, AndroidX, Material 3 (Apache 2.0)</string>
|
||||
<string name="about_contributing_title">Přispívání</string>
|
||||
<string name="about_source_code">Zdrojový kód</string>
|
||||
<string name="about_issues">Nahlásit problém</string>
|
||||
</resources>
|
||||
|
||||
@ -57,20 +57,23 @@
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="det_settings_title">Settings</string>
|
||||
<string name="det_biometric_toggle">Biometric Unlock</string>
|
||||
<string name="det_biometric_toggle_desc">Use fingerprint or face to unlock the vault</string>
|
||||
<string name="det_lock_timeout">Auto-lock timeout</string>
|
||||
<string name="det_timeout_off">Disabled</string>
|
||||
<string name="det_timeout_minutes">%d minutes</string>
|
||||
<string name="det_argon2_info_title">Argon2id Parameters</string>
|
||||
<string name="det_argon2_info_body">Memory: 64 MB · Iterations: 3 · Parallelism: 1 · Output: 64 bytes</string>
|
||||
|
||||
<!-- Lock screen -->
|
||||
<string name="det_locked_title">Vault Locked</string>
|
||||
<string name="det_locked_body">The vault was locked due to inactivity.</string>
|
||||
<string name="det_unlock_biometric">Unlock with Biometrics</string>
|
||||
<string name="det_biometric_prompt_title">Unlock Vault</string>
|
||||
<string name="det_biometric_prompt_subtitle">Authenticate to access your vault</string>
|
||||
<string name="det_export_locked_body">Authenticate to view your device secret QR backup.</string>
|
||||
<string name="det_import_biometric_subtitle">Confirm replacing your device secret</string>
|
||||
|
||||
<!-- Privacy -->
|
||||
<string name="privacy_title">Privacy</string>
|
||||
<string name="privacy_body">Password Zebra operates entirely offline. No data is collected, transmitted, or stored outside your device. Camera access is used solely to scan QR codes locally. Biometric authentication is handled entirely by the Android operating system and no biometric data is accessible to this app.</string>
|
||||
|
||||
<!-- About / License -->
|
||||
<string name="about_title">About</string>
|
||||
<string name="about_version">Version %s</string>
|
||||
<string name="about_license_title">License</string>
|
||||
<string name="about_license_body">This app is free software licensed under the GNU General Public License v3.0 (GPL-3.0-or-later). You may redistribute and/or modify it under the terms of the GPL as published by the Free Software Foundation, version 3 or (at your option) any later version.\n\nThis program is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</string>
|
||||
<string name="about_oss_title">Open-source components</string>
|
||||
<string name="about_oss_body">Bouncy Castle (MIT) · ZXing / ZXing Android Embedded (Apache 2.0) · Jetpack Compose, AndroidX, Material 3 (Apache 2.0)</string>
|
||||
<string name="about_contributing_title">Contributing</string>
|
||||
<string name="about_source_code">Source code</string>
|
||||
<string name="about_issues">Report an issue</string>
|
||||
</resources>
|
||||
|
||||
133
docs/privacy-policy.html
Normal file
133
docs/privacy-policy.html
Normal file
@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Privacy Policy – Password Zebra</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
max-width: 720px;
|
||||
margin: 40px auto;
|
||||
padding: 0 24px 60px;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.7;
|
||||
}
|
||||
h1 { font-size: 1.8rem; margin-bottom: 0.25em; }
|
||||
h2 { font-size: 1.1rem; margin-top: 2em; color: #333; }
|
||||
.meta { color: #666; font-size: 0.9rem; margin-bottom: 2.5em; }
|
||||
.lang-divider {
|
||||
border: none;
|
||||
border-top: 2px solid #e0e0e0;
|
||||
margin: 3em 0;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background: #121212; color: #e0e0e0; }
|
||||
h2 { color: #bbb; }
|
||||
.meta { color: #888; }
|
||||
.lang-divider { border-color: #333; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Privacy Policy</h1>
|
||||
<p class="meta">Password Zebra | Last updated: 2026-03-06</p>
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>
|
||||
Password Zebra is a fully <strong>offline</strong> application.
|
||||
It does not connect to the internet, does not collect any personal data,
|
||||
and does not transmit any information to external servers or third parties.
|
||||
</p>
|
||||
|
||||
<h2>Data stored on your device</h2>
|
||||
<p>The following data is stored <em>locally on your device only</em>,
|
||||
encrypted via the Android Keystore system:</p>
|
||||
<ul>
|
||||
<li><strong>Device secret</strong> — a random value used together with your master password
|
||||
to derive deterministic passwords. It never leaves the device unless you explicitly
|
||||
export it via the QR backup feature.</li>
|
||||
<li><strong>Service names and rotation counters</strong> — the list of services you have
|
||||
used and any password rotation counts. Stored in encrypted SharedPreferences.</li>
|
||||
</ul>
|
||||
<p>Your master password is <strong>never stored</strong>. It is held in memory only
|
||||
for the duration of password generation and immediately wiped afterwards.</p>
|
||||
|
||||
<h2>Camera</h2>
|
||||
<p>Camera access is requested solely to scan QR codes for the import feature.
|
||||
No images are saved, transmitted, or processed outside the app.</p>
|
||||
|
||||
<h2>Biometric authentication</h2>
|
||||
<p>Biometric authentication (fingerprint, face unlock, device PIN/pattern) is handled
|
||||
entirely by the Android operating system via the BiometricPrompt API.
|
||||
The app receives only a success/failure result and has no access to any biometric data.</p>
|
||||
|
||||
<h2>Permissions</h2>
|
||||
<ul>
|
||||
<li><code>USE_BIOMETRIC</code> / <code>USE_FINGERPRINT</code> — biometric lock</li>
|
||||
<li><code>CAMERA</code> — QR code scanning (import feature)</li>
|
||||
</ul>
|
||||
<p>No network, location, contacts, or storage permissions are used.</p>
|
||||
|
||||
<h2>Third-party libraries</h2>
|
||||
<p>Password Zebra uses only open-source libraries that run locally and perform no
|
||||
network activity: Bouncy Castle (cryptography), ZXing (QR codes), Jetpack / AndroidX.</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>Questions or concerns: open an issue at
|
||||
<a href="https://git.bugsy.cz/beval/password_zebra/issues">git.bugsy.cz/beval/password_zebra/issues</a>.
|
||||
</p>
|
||||
|
||||
<hr class="lang-divider">
|
||||
|
||||
<h1>Zásady ochrany soukromí</h1>
|
||||
<p class="meta">Password Zebra | Poslední aktualizace: 2026-03-06</p>
|
||||
|
||||
<h2>Přehled</h2>
|
||||
<p>
|
||||
Password Zebra je zcela <strong>offline</strong> aplikace.
|
||||
Nepřipojuje se k internetu, neshromažďuje žádné osobní údaje
|
||||
a nepřenáší žádné informace na externí servery ani třetím stranám.
|
||||
</p>
|
||||
|
||||
<h2>Data uložená ve vašem zařízení</h2>
|
||||
<p>Následující data jsou uložena <em>výhradně ve vašem zařízení</em>,
|
||||
šifrována prostřednictvím systému Android Keystore:</p>
|
||||
<ul>
|
||||
<li><strong>Device secret</strong> — náhodná hodnota používaná spolu s vaším hlavním heslem
|
||||
k odvozování deterministických hesel. Zařízení nikdy neopustí, pokud jej explicitně
|
||||
neexportujete prostřednictvím funkce QR zálohy.</li>
|
||||
<li><strong>Názvy služeb a čítače rotací</strong> — seznam použitých služeb a případné
|
||||
čítače rotace hesel. Uloženo v šifrovaných SharedPreferences.</li>
|
||||
</ul>
|
||||
<p>Vaše hlavní heslo <strong>není nikdy uloženo</strong>. Je drženo v paměti pouze
|
||||
po dobu generování hesla a poté okamžitě vymazáno.</p>
|
||||
|
||||
<h2>Kamera</h2>
|
||||
<p>Přístup ke kameře je vyžadován výhradně ke skenování QR kódů pro funkci importu.
|
||||
Žádné snímky nejsou ukládány, přenášeny ani zpracovávány mimo aplikaci.</p>
|
||||
|
||||
<h2>Biometrické ověření</h2>
|
||||
<p>Biometrické ověření (otisk prstu, odemčení obličejem, PIN/gesto) je plně zajišťováno
|
||||
operačním systémem Android prostřednictvím rozhraní BiometricPrompt API.
|
||||
Aplikace obdrží pouze výsledek úspěch/selhání a nemá přístup k žádným biometrickým datům.</p>
|
||||
|
||||
<h2>Oprávnění</h2>
|
||||
<ul>
|
||||
<li><code>USE_BIOMETRIC</code> / <code>USE_FINGERPRINT</code> — biometrický zámek</li>
|
||||
<li><code>CAMERA</code> — skenování QR kódů (funkce importu)</li>
|
||||
</ul>
|
||||
<p>Nejsou použita žádná síťová, polohová, kontaktní ani úložišční oprávnění.</p>
|
||||
|
||||
<h2>Open-source knihovny</h2>
|
||||
<p>Password Zebra používá pouze open-source knihovny fungující lokálně bez síťové aktivity:
|
||||
Bouncy Castle (kryptografie), ZXing (QR kódy), Jetpack / AndroidX.</p>
|
||||
|
||||
<h2>Kontakt</h2>
|
||||
<p>Dotazy nebo připomínky: otevřete issue na
|
||||
<a href="https://git.bugsy.cz/beval/password_zebra/issues">git.bugsy.cz/beval/password_zebra/issues</a>.
|
||||
</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user