From 0291433725056a3f4e712d343183001ecb2a265a Mon Sep 17 00:00:00 2001 From: Pavel Baksy Date: Fri, 6 Mar 2026 20:21:49 +0100 Subject: [PATCH] Add GPL v3 license, privacy policy, contributing links, release signing --- .gitignore | 3 + LICENSE | 20 +++ app/build.gradle.kts | 18 ++ .../ui/screens/SettingsScreen.kt | 163 ++++++++---------- app/src/main/res/values-cs/strings.xml | 27 +-- app/src/main/res/values/strings.xml | 27 +-- docs/privacy-policy.html | 133 ++++++++++++++ 7 files changed, 273 insertions(+), 118 deletions(-) create mode 100644 LICENSE create mode 100644 docs/privacy-policy.html diff --git a/.gitignore b/.gitignore index e174065..7f61d32 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ local.properties .claude CLAUDE.md +keystore.properties +*.jks +*.keystore diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..90b92c7 --- /dev/null +++ b/LICENSE @@ -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 . + +--- + +[Append the full text of the GNU General Public License v3 below. + Download it from: https://www.gnu.org/licenses/gpl-3.0.txt] diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1073ce0..d946656 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" diff --git a/app/src/main/java/cz/bugsy/passwordzebra/ui/screens/SettingsScreen.kt b/app/src/main/java/cz/bugsy/passwordzebra/ui/screens/SettingsScreen.kt index 58b915b..034f866 100644 --- a/app/src/main/java/cz/bugsy/passwordzebra/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/cz/bugsy/passwordzebra/ui/screens/SettingsScreen.kt @@ -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)) } } diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index ed02aa2..0ee5e26 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -58,20 +58,23 @@ Nastavení - Biometrické odemčení - Odemkněte trezor otiskem prstu nebo tváří - Automatické zamčení - Vypnuto - %d minut Parametry Argon2id Paměť: 64 MB · Iterace: 3 · Paralelismus: 1 · Výstup: 64 bajtů - - - Trezor zamčen - Trezor byl zamčen z důvodu nečinnosti. - Odemknout biometrikou - Odemknout trezor - Ověřte se pro přístup do trezoru Ověřte se pro zobrazení QR zálohy vašeho device secret. Potvrzení nahrazení device secret + + + Ochrana soukromí + 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. + + + O aplikaci + Verze %s + Licence + 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. + Open-source komponenty + Bouncy Castle (MIT) · ZXing / ZXing Android Embedded (Apache 2.0) · Jetpack Compose, AndroidX, Material 3 (Apache 2.0) + Přispívání + Zdrojový kód + Nahlásit problém diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9808f9a..e122208 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -57,20 +57,23 @@ Settings - Biometric Unlock - Use fingerprint or face to unlock the vault - Auto-lock timeout - Disabled - %d minutes Argon2id Parameters Memory: 64 MB · Iterations: 3 · Parallelism: 1 · Output: 64 bytes - - - Vault Locked - The vault was locked due to inactivity. - Unlock with Biometrics - Unlock Vault - Authenticate to access your vault Authenticate to view your device secret QR backup. Confirm replacing your device secret + + + Privacy + 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. + + + About + Version %s + License + 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. + Open-source components + Bouncy Castle (MIT) · ZXing / ZXing Android Embedded (Apache 2.0) · Jetpack Compose, AndroidX, Material 3 (Apache 2.0) + Contributing + Source code + Report an issue diff --git a/docs/privacy-policy.html b/docs/privacy-policy.html new file mode 100644 index 0000000..112c29e --- /dev/null +++ b/docs/privacy-policy.html @@ -0,0 +1,133 @@ + + + + + + Privacy Policy – Password Zebra + + + + +

Privacy Policy

+

Password Zebra  |  Last updated: 2026-03-06

+ +

Overview

+

+ Password Zebra is a fully offline 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. +

+ +

Data stored on your device

+

The following data is stored locally on your device only, + encrypted via the Android Keystore system:

+ +

Your master password is never stored. It is held in memory only + for the duration of password generation and immediately wiped afterwards.

+ +

Camera

+

Camera access is requested solely to scan QR codes for the import feature. + No images are saved, transmitted, or processed outside the app.

+ +

Biometric authentication

+

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.

+ +

Permissions

+ +

No network, location, contacts, or storage permissions are used.

+ +

Third-party libraries

+

Password Zebra uses only open-source libraries that run locally and perform no + network activity: Bouncy Castle (cryptography), ZXing (QR codes), Jetpack / AndroidX.

+ +

Contact

+

Questions or concerns: open an issue at + git.bugsy.cz/beval/password_zebra/issues. +

+ +
+ +

Zásady ochrany soukromí

+

Password Zebra  |  Poslední aktualizace: 2026-03-06

+ +

Přehled

+

+ Password Zebra je zcela offline 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. +

+ +

Data uložená ve vašem zařízení

+

Následující data jsou uložena výhradně ve vašem zařízení, + šifrována prostřednictvím systému Android Keystore:

+ +

Vaše hlavní heslo není nikdy uloženo. Je drženo v paměti pouze + po dobu generování hesla a poté okamžitě vymazáno.

+ +

Kamera

+

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.

+ +

Biometrické ověření

+

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.

+ +

Oprávnění

+ +

Nejsou použita žádná síťová, polohová, kontaktní ani úložišční oprávnění.

+ +

Open-source knihovny

+

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.

+ +

Kontakt

+

Dotazy nebo připomínky: otevřete issue na + git.bugsy.cz/beval/password_zebra/issues. +

+ + +