From 7b7123eb657bc5e5b443f3b54c22a16d042dd874 Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Tue, 1 Aug 2023 00:55:21 -0600 Subject: Secret editing + backup finished + Compose previews + lots of improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Iván Ávalos --- anastasis/build.gradle.kts | 2 +- .../main/java/net/taler/anastasis/MainActivity.kt | 10 +- .../src/main/java/net/taler/anastasis/Routes.kt | 4 +- .../net/taler/anastasis/reducers/ReducerManager.kt | 2 +- .../anastasis/ui/backup/BackupFinishedScreen.kt | 196 +++++++++++++++++++++ .../taler/anastasis/ui/backup/EditSecretScreen.kt | 110 ++++++++++++ .../anastasis/ui/backup/ReviewPoliciesScreen.kt | 98 +++++++++-- .../anastasis/ui/backup/SelectAuthMethodsScreen.kt | 69 +++++++- .../anastasis/ui/common/SelectContinentScreen.kt | 26 ++- .../anastasis/ui/common/SelectCountryScreen.kt | 34 +++- .../ui/common/SelectUserAttributesScreen.kt | 60 +++++-- .../taler/anastasis/ui/dialogs/EditMethodDialog.kt | 8 +- .../net/taler/anastasis/ui/forms/EditSecretForm.kt | 85 +++++++++ .../java/net/taler/anastasis/ui/home/HomeScreen.kt | 4 +- .../taler/anastasis/viewmodels/ReducerViewModel.kt | 58 ++++-- anastasis/src/main/res/values/strings.xml | 4 + .../src/main/java/net/taler/common/Bech32.kt | 2 +- .../src/main/java/net/taler/common/CryptoUtils.kt | 97 ++++++++++ .../src/main/java/net/taler/common/CyptoUtils.kt | 71 -------- 19 files changed, 818 insertions(+), 122 deletions(-) create mode 100644 anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupFinishedScreen.kt create mode 100644 anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt create mode 100644 anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt create mode 100644 taler-kotlin-android/src/main/java/net/taler/common/CryptoUtils.kt delete mode 100644 taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt diff --git a/anastasis/build.gradle.kts b/anastasis/build.gradle.kts index eedf4f2..52a9ec5 100644 --- a/anastasis/build.gradle.kts +++ b/anastasis/build.gradle.kts @@ -27,7 +27,7 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } diff --git a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt index 653d8a5..80b74bf 100644 --- a/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt +++ b/anastasis/src/main/java/net/taler/anastasis/MainActivity.kt @@ -15,6 +15,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import dagger.hilt.android.AndroidEntryPoint +import net.taler.anastasis.ui.backup.BackupFinishedScreen +import net.taler.anastasis.ui.backup.EditSecretScreen import net.taler.anastasis.ui.backup.ReviewPoliciesScreen import net.taler.anastasis.ui.backup.SelectAuthMethodsScreen import net.taler.anastasis.ui.common.SelectContinentScreen @@ -79,9 +81,15 @@ fun MainNavHost( Routes.SelectAuthMethods.route -> { SelectAuthMethodsScreen() } - Routes.ReviewPoliciesScreen.route -> { + Routes.ReviewPolicies.route -> { ReviewPoliciesScreen() } + Routes.EditSecret.route -> { + EditSecretScreen() + } + Routes.BackupFinished.route -> { + BackupFinishedScreen() + } Routes.RestoreInit.route -> { Text("This is the restore session screen!") } diff --git a/anastasis/src/main/java/net/taler/anastasis/Routes.kt b/anastasis/src/main/java/net/taler/anastasis/Routes.kt index 5258e7f..e3242af 100644 --- a/anastasis/src/main/java/net/taler/anastasis/Routes.kt +++ b/anastasis/src/main/java/net/taler/anastasis/Routes.kt @@ -13,7 +13,9 @@ sealed class Routes( // Backup object SelectAuthMethods: Routes("select_auth_methods") - object ReviewPoliciesScreen: Routes("review_policies") + object ReviewPolicies: Routes("review_policies") + object EditSecret: Routes("edit_secret") + object BackupFinished: Routes("backup_finished") // Restore object RestoreInit: Routes("restore") diff --git a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt index a074044..b3483d5 100644 --- a/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt +++ b/anastasis/src/main/java/net/taler/anastasis/reducers/ReducerManager.kt @@ -218,7 +218,7 @@ class ReducerManager( this@ReducerManager.next() }.onError { onError(it) } } - } + }.onError { onError(it) } } } } \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupFinishedScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupFinishedScreen.kt new file mode 100644 index 0000000..fc89caa --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/BackupFinishedScreen.kt @@ -0,0 +1,196 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler 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, or (at your option) any later version. + * + * GNU Taler 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 + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.anastasis.ui.backup + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CloudDone +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import net.taler.anastasis.R +import net.taler.anastasis.models.AuthenticationProviderStatus +import net.taler.anastasis.models.BackupStates +import net.taler.anastasis.models.ReducerState +import net.taler.anastasis.models.SuccessDetail +import net.taler.anastasis.shared.Utils +import net.taler.anastasis.ui.reusable.pages.WizardPage +import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.FakeReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModelI +import net.taler.common.Timestamp + +@Composable +fun BackupFinishedScreen( + viewModel: ReducerViewModelI = hiltViewModel(), +) { + val state by viewModel.reducerState.collectAsState() + val reducerState = state as? ReducerState.Backup + ?: error("invalid reducer state type") + + // Get only providers with "ok" status + val providers = remember(reducerState.authenticationProviders) { + reducerState.authenticationProviders?.filter { + it.value is AuthenticationProviderStatus.Ok + }?.mapValues { it.value as AuthenticationProviderStatus.Ok } ?: emptyMap() + } + + val details = reducerState.successDetails ?: emptyMap() + + WizardPage( + title = stringResource(R.string.backup_finished_title), + onBackClicked = { viewModel.goHome() }, + showNext = false, + showPrev = false, + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(it), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { + Box( + modifier = Modifier + .padding(LocalSpacing.current.large) + .background(MaterialTheme.colorScheme.primary, shape = CircleShape) + .fillMaxWidth(0.4f) + .aspectRatio(1f), + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Default.CloudDone, + modifier = Modifier.fillMaxSize(0.5f), + tint = MaterialTheme.colorScheme.onPrimary, + contentDescription = stringResource(R.string.success), + ) + } + } + item { + Text( + stringResource(R.string.backup_stored_providers), + modifier = Modifier.padding(LocalSpacing.current.medium), + style = MaterialTheme.typography.titleLarge, + ) + } + items(items = details.keys.toList()) { url -> + val provider = providers[url] ?: return@items + val detail = details[url] ?: return@items + ProviderCard( + modifier = Modifier + .padding( + start = LocalSpacing.current.medium, + end = LocalSpacing.current.medium, + bottom = LocalSpacing.current.small, + ) + .fillMaxWidth(), + provider = provider, + detail = detail, + ) + } + } + } +} + +@Composable +fun ProviderCard( + modifier: Modifier, + provider: AuthenticationProviderStatus.Ok, + detail: SuccessDetail, +) { + ElevatedCard( + modifier = modifier, + ) { + Column( + modifier = Modifier.padding(LocalSpacing.current.medium) + ) { + val date = Utils.formatDate( + Instant + .fromEpochMilliseconds(detail.policyExpiration.ms) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .date, + ) + Text( + provider.businessName, + modifier = Modifier.padding(bottom = LocalSpacing.current.small), + style = MaterialTheme.typography.titleMedium, + ) + Text( + stringResource( + R.string.backup_policy_detail, + detail.policyVersion, + date, + ) + ) + } + } +} + +@Preview +@Composable +fun BackupFinishedScreenPreview() { + BackupFinishedScreen( + viewModel = FakeReducerViewModel( + state = ReducerState.Backup( + backupState = BackupStates.BackupFinished, + authenticationProviders = mapOf( + "https://localhost:8088/" to AuthenticationProviderStatus.Ok( + httpStatus = 200, + methods = listOf(), + annualFee = "EUR:0.99", + truthUploadFee = "EUR:3.99", + liabilityLimit = "EUR:1", + currency = "EUR", + storageLimitInMegabytes = 1, + businessName = "Anastasis 42", + providerSalt = "BXAPCKSH9D3MYJTS9536RHJHCX", + ) + ), + successDetails = mapOf( + "https://localhost:8088/" to SuccessDetail( + policyVersion = 1, + policyExpiration = Timestamp.now(), + ), + ), + ), + ), + ) +} \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt new file mode 100644 index 0000000..9706ceb --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/EditSecretScreen.kt @@ -0,0 +1,110 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler 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, or (at your option) any later version. + * + * GNU Taler 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 + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.anastasis.ui.backup + +import androidx.compose.foundation.layout.fillMaxSize +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.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.hilt.navigation.compose.hiltViewModel +import net.taler.anastasis.R +import net.taler.anastasis.models.BackupStates +import net.taler.anastasis.models.CoreSecret +import net.taler.anastasis.models.ReducerArgs +import net.taler.anastasis.models.ReducerState +import net.taler.anastasis.ui.forms.EditSecretForm +import net.taler.anastasis.ui.reusable.pages.WizardPage +import net.taler.anastasis.viewmodels.FakeReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModelI +import net.taler.common.Amount +import net.taler.common.CryptoUtils +import net.taler.common.Timestamp + +@Composable +fun EditSecretScreen( + viewModel: ReducerViewModelI = hiltViewModel(), +) { + val state by viewModel.reducerState.collectAsState() + val reducerState = state as? ReducerState.Backup + ?: error("invalid reducer state type") + + var secretName by remember { + mutableStateOf(reducerState.secretName ?: "") + } + var secretValue by remember { + mutableStateOf(reducerState.coreSecret?.value?.let { + CryptoUtils.decodeCrock(it).toString(Charsets.UTF_8) + } ?: "") + } + + WizardPage( + title = stringResource(R.string.edit_secret_title), + onBackClicked = { viewModel.goHome() }, + onPrevClicked = { viewModel.goBack() }, + onNextClicked = { + viewModel.reducerManager?.backupSecret( + name = secretName, + args = ReducerArgs.EnterSecret( + secret = ReducerArgs.EnterSecret.Secret( + value = CryptoUtils.encodeCrock(secretValue.toByteArray(Charsets.UTF_8)), + mime = "text/plain", + ), + expiration = null, + ) + ) + }, + ) { + EditSecretForm( + modifier = Modifier.fillMaxSize(), + name = secretName, + value = secretValue, + ) { name, value, _ -> + secretName = name + secretValue = value + } + } +} + +@Preview +@Composable +fun EditSecretScreenPreview() { + EditSecretScreen( + viewModel = FakeReducerViewModel( + state = ReducerState.Backup( + backupState = BackupStates.SecretEditing, + secretName = "_TALERWALLET_MyPinePhone", + coreSecret = CoreSecret( + value = "EDJP6WK5EG50", + mime = "text/plain", + ), + expiration = Timestamp.never(), + uploadFees = listOf( + ReducerState.Backup.UploadFee( + fee = Amount("KUDOS", 42L, 0), + ), + ), + ), + ), + ) +} \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt index c42e7bc..edd6ddb 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/ReviewPoliciesScreen.kt @@ -49,22 +49,27 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R import net.taler.anastasis.models.AuthMethod import net.taler.anastasis.models.AuthenticationProviderStatus +import net.taler.anastasis.models.BackupStates +import net.taler.anastasis.models.MethodSpec import net.taler.anastasis.models.Policy import net.taler.anastasis.models.ReducerState import net.taler.anastasis.ui.dialogs.EditPolicyDialog import net.taler.anastasis.ui.forms.EditPolicyForm import net.taler.anastasis.ui.reusable.pages.WizardPage import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.FakeReducerViewModel import net.taler.anastasis.viewmodels.ReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModelI @OptIn(ExperimentalMaterial3Api::class) @Composable fun ReviewPoliciesScreen( - viewModel: ReducerViewModel = hiltViewModel(), + viewModel: ReducerViewModelI = hiltViewModel(), ) { val state by viewModel.reducerState.collectAsState() val reducerState = state as? ReducerState.Backup @@ -97,9 +102,9 @@ fun ReviewPoliciesScreen( onCancel = { reset() }, onPolicyEdited = { editingPolicyIndex?.let { index -> - viewModel.reducerManager.updatePolicy(index, it) + viewModel.reducerManager?.updatePolicy(index, it) } ?: run { - viewModel.reducerManager.addPolicy(it) + viewModel.reducerManager?.addPolicy(it) } reset() } @@ -111,7 +116,7 @@ fun ReviewPoliciesScreen( onBackClicked = { viewModel.goHome() }, onPrevClicked = { viewModel.goBack() }, onNextClicked = { - viewModel.reducerManager.next() + viewModel.reducerManager?.next() } ) { Scaffold( @@ -143,10 +148,10 @@ fun ReviewPoliciesScreen( providers = providers, index = index, onEdit = { - viewModel.reducerManager.updatePolicy(index, it) + viewModel.reducerManager?.updatePolicy(index, it) }, ) { - viewModel.reducerManager.deletePolicy(index) + viewModel.reducerManager?.deletePolicy(index) } } } @@ -249,11 +254,84 @@ fun PolicyMethodCard( style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold, ) -// Text( -// provider.businessName, -// style = MaterialTheme.typography.labelMedium, -// ) } } } +} + +@Preview +@Composable +fun ReviewPoliciesScreenPreview() { + ReviewPoliciesScreen( + viewModel = FakeReducerViewModel( + state = ReducerState.Backup( + backupState = BackupStates.PoliciesReviewing, + authenticationMethods = listOf( + AuthMethod( + type = AuthMethod.Type.Question, + mimeType = "text/plain", + challenge = "E1QPPS8A", + instructions = "What is your favorite GNU package?", + ), + AuthMethod( + type = AuthMethod.Type.Email, + instructions = "E-mail to user@*le.com", + challenge = "ENSPAWJ0CNW62VBGDHJJWRVFDM50", + ) + ), + authenticationProviders = mapOf( + "http://localhost:8088/" to AuthenticationProviderStatus.Ok( + httpStatus = 200, + methods = listOf( + MethodSpec(type = AuthMethod.Type.Question, usageFee = "EUR:0.001"), + MethodSpec(type = AuthMethod.Type.Sms, usageFee = "EUR:0.55"), + ), + annualFee = "EUR:0.99", + truthUploadFee = "EUR:3.99", + liabilityLimit = "EUR:1", + currency = "EUR", + storageLimitInMegabytes = 1, + businessName = "Anastasis 4", + providerSalt = "CXAPCKSH9D3MYJTS9536RHJHCW", + ), + "http://localhost:8089/" to AuthenticationProviderStatus.Ok( + httpStatus = 200, + methods = listOf( + MethodSpec(type = AuthMethod.Type.Question, usageFee = "EUR:0.001"), + MethodSpec(type = AuthMethod.Type.Sms, usageFee = "EUR:0.55"), + ), + annualFee = "EUR:0.99", + truthUploadFee = "EUR:3.99", + liabilityLimit = "EUR:1", + currency = "EUR", + storageLimitInMegabytes = 1, + businessName = "Anastasis 2", + providerSalt = "CXAPCKSH9D3MYJTS9536RHJHCW", + ), + ), + policies = listOf( + Policy( + methods = listOf( + Policy.PolicyMethod( + authenticationMethod = 0, + provider = "http://localhost:8089/", + ), + Policy.PolicyMethod( + authenticationMethod = 1, + provider = "http://localhost:8088/", + ), + ), + ), + Policy( + methods = listOf( + Policy.PolicyMethod( + authenticationMethod = 0, + provider = "http://localhost:8089/", + ), + ), + ), + ), + ), + ), + ) } \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt index 1ffd9ac..7cbea00 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/backup/SelectAuthMethodsScreen.kt @@ -45,23 +45,28 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R import net.taler.anastasis.shared.Utils import net.taler.anastasis.models.AuthMethod import net.taler.anastasis.models.AuthenticationProviderStatus +import net.taler.anastasis.models.BackupStates +import net.taler.anastasis.models.MethodSpec import net.taler.anastasis.models.ReducerArgs import net.taler.anastasis.models.ReducerState import net.taler.anastasis.ui.dialogs.EditMethodDialog import net.taler.anastasis.ui.reusable.components.ActionCard import net.taler.anastasis.ui.reusable.pages.WizardPage import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.FakeReducerViewModel import net.taler.anastasis.viewmodels.ReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModelI @Composable fun SelectAuthMethodsScreen( - viewModel: ReducerViewModel = hiltViewModel(), + viewModel: ReducerViewModelI = hiltViewModel(), ) { val state by viewModel.reducerState.collectAsState() val reducerState = state as? ReducerState.Backup @@ -99,7 +104,7 @@ fun SelectAuthMethodsScreen( onMethodEdited = { reset() Log.d("onMethodEdited", it.challenge) - viewModel.reducerManager.addAuthentication( + viewModel.reducerManager?.addAuthentication( ReducerArgs.AddAuthentication(it) ) } @@ -111,7 +116,7 @@ fun SelectAuthMethodsScreen( onBackClicked = { viewModel.goHome() }, onPrevClicked = { viewModel.goBack() }, onNextClicked = { - viewModel.reducerManager.next() + viewModel.reducerManager?.next() } ) { scroll -> AuthMethods( @@ -123,7 +128,7 @@ fun SelectAuthMethodsScreen( showEditDialog = true }, onDeleteMethod = { - viewModel.reducerManager.deleteAuthentication(it) + viewModel.reducerManager?.deleteAuthentication(it) }, ) } @@ -230,4 +235,60 @@ private fun ChallengeCard( } } } +} + +@Preview +@Composable +fun SelectAuthMethodsScreenPreview() { + SelectAuthMethodsScreen( + viewModel = FakeReducerViewModel( + state = ReducerState.Backup( + backupState = BackupStates.AuthenticationsEditing, + authenticationMethods = listOf( + AuthMethod( + type = AuthMethod.Type.Question, + mimeType = "text/plain", + challenge = "E1QPPS8A", + instructions = "What is your favorite GNU package?", + ), + AuthMethod( + type = AuthMethod.Type.Email, + instructions = "E-mail to user@*le.com", + challenge = "ENSPAWJ0CNW62VBGDHJJWRVFDM50", + ) + ), + authenticationProviders = mapOf( + "http://localhost:8088/" to AuthenticationProviderStatus.Ok( + httpStatus = 200, + methods = listOf( + MethodSpec(type = AuthMethod.Type.Question, usageFee = "EUR:0.001"), + MethodSpec(type = AuthMethod.Type.Sms, usageFee = "EUR:0.55"), + ), + annualFee = "EUR:0.99", + truthUploadFee = "EUR:3.99", + liabilityLimit = "EUR:1", + currency = "EUR", + storageLimitInMegabytes = 1, + businessName = "Anastasis 4", + providerSalt = "CXAPCKSH9D3MYJTS9536RHJHCW", + ), + "http://localhost:8089/" to AuthenticationProviderStatus.Ok( + httpStatus = 200, + methods = listOf( + MethodSpec(type = AuthMethod.Type.Question, usageFee = "EUR:0.001"), + MethodSpec(type = AuthMethod.Type.Sms, usageFee = "EUR:0.55"), + ), + annualFee = "EUR:0.99", + truthUploadFee = "EUR:3.99", + liabilityLimit = "EUR:1", + currency = "EUR", + storageLimitInMegabytes = 1, + businessName = "Anastasis 2", + providerSalt = "CXAPCKSH9D3MYJTS9536RHJHCW", + ), + ), + + ) + ) + ) } \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt index eacbea4..10a8978 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectContinentScreen.kt @@ -28,17 +28,22 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R +import net.taler.anastasis.models.BackupStates +import net.taler.anastasis.models.ContinentInfo import net.taler.anastasis.models.ReducerState import net.taler.anastasis.ui.reusable.components.Picker import net.taler.anastasis.ui.reusable.pages.WizardPage import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.FakeReducerViewModel import net.taler.anastasis.viewmodels.ReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModelI @Composable fun SelectContinentScreen( - viewModel: ReducerViewModel = hiltViewModel(), + viewModel: ReducerViewModelI = hiltViewModel(), ) { val reducerState by viewModel.reducerState.collectAsState() val continents = when (val state = reducerState) { @@ -66,7 +71,7 @@ fun SelectContinentScreen( onBackClicked = { viewModel.goHome() }, onNextClicked = { localContinent?.let { - viewModel.reducerManager.selectContinent(it) + viewModel.reducerManager?.selectContinent(it) } }, ) { @@ -88,4 +93,21 @@ fun SelectContinentScreen( ) } } +} + +@Preview +@Composable +fun SelectContinentScreenPreview() { + SelectContinentScreen( + viewModel = FakeReducerViewModel( + state = ReducerState.Backup( + backupState = BackupStates.ContinentSelecting, + selectedContinent = "Europe", + continents = listOf( + ContinentInfo(name = "Europe"), + ContinentInfo(name = "North America"), + ) + ) + ) + ) } \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt index 4c8a32e..f137e5e 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectCountryScreen.kt @@ -28,17 +28,22 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import net.taler.anastasis.R +import net.taler.anastasis.models.BackupStates +import net.taler.anastasis.models.CountryInfo import net.taler.anastasis.models.ReducerState import net.taler.anastasis.ui.reusable.components.Picker import net.taler.anastasis.ui.reusable.pages.WizardPage import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.FakeReducerViewModel import net.taler.anastasis.viewmodels.ReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModelI @Composable fun SelectCountryScreen( - viewModel: ReducerViewModel = hiltViewModel(), + viewModel: ReducerViewModelI = hiltViewModel(), ) { val reducerState by viewModel.reducerState.collectAsState() val countries = when (val state = reducerState) { @@ -66,7 +71,7 @@ fun SelectCountryScreen( onPrevClicked = { viewModel.goBack() }, onNextClicked = { localCountry?.let { - viewModel.reducerManager.selectCountry(it) + viewModel.reducerManager?.selectCountry(it) } }, ) { @@ -88,4 +93,29 @@ fun SelectCountryScreen( ) } } +} + +@Preview +@Composable +fun SelectCountryScreenPreview() { + SelectCountryScreen( + viewModel = FakeReducerViewModel( + state = ReducerState.Backup( + backupState = BackupStates.ContinentSelecting, + selectedCountry = "ch", + countries = listOf( + CountryInfo( + code = "ch", + name = "Switzerland", + continent = "Europe", + ), + CountryInfo( + code = "de", + name = "Germany", + continent = "Europe", + ) + ) + ) + ) + ) } \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt index 9374798..6c09ae5 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/common/SelectUserAttributesScreen.kt @@ -24,6 +24,8 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text @@ -34,23 +36,29 @@ import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import kotlinx.datetime.toLocalDate import net.taler.anastasis.R -import net.taler.anastasis.shared.Utils +import net.taler.anastasis.models.BackupStates import net.taler.anastasis.models.ReducerState import net.taler.anastasis.models.UserAttributeSpec import net.taler.anastasis.shared.FieldStatus +import net.taler.anastasis.shared.Utils import net.taler.anastasis.ui.reusable.components.DatePickerField import net.taler.anastasis.ui.reusable.pages.WizardPage import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.anastasis.viewmodels.FakeReducerViewModel import net.taler.anastasis.viewmodels.ReducerViewModel +import net.taler.anastasis.viewmodels.ReducerViewModelI @OptIn(ExperimentalMaterial3Api::class) @Composable fun SelectUserAttributesScreen( - viewModel: ReducerViewModel = hiltViewModel(), + viewModel: ReducerViewModelI = hiltViewModel(), ) { val reducerState by viewModel.reducerState.collectAsState() val userAttributes = when (val state = reducerState) { @@ -69,18 +77,18 @@ fun SelectUserAttributesScreen( *identityAttributes.toList().toTypedArray() ) } - val enableNext = remember(userAttributes, values) { - userAttributes.fold(true) { a, b -> - a && (fieldStatus(b, values[b.name]) == FieldStatus.Valid) - } + val enableNext = userAttributes.fold(true) { a, b -> + a && (fieldStatus(b, values[b.name]) == FieldStatus.Valid) } + val focusManager = LocalFocusManager.current + WizardPage( title = stringResource(R.string.select_user_attributes_title), onBackClicked = { viewModel.goHome() }, onPrevClicked = { viewModel.goBack() }, onNextClicked = { - viewModel.reducerManager.enterUserAttributes(values) + viewModel.reducerManager?.enterUserAttributes(values) }, enableNext = enableNext, ) { scrollConnection -> @@ -91,9 +99,7 @@ fun SelectUserAttributesScreen( verticalArrangement = Arrangement.Top, ) { items(items = userAttributes) { attr -> - val status = remember(attr, values) { - fieldStatus(attr, values[attr.name]) - } + val status = fieldStatus(attr, values[attr.name]) val supportingRes = remember(attr, status) { status.msgRes ?: if (attr.optional == true) { R.string.field_optional @@ -108,6 +114,8 @@ fun SelectUserAttributesScreen( ) .fillMaxWidth(), value = values[attr.name] ?: "", + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }), onValueChange = { values[attr.name] = it }, isError = status.error, supportingText = { @@ -152,4 +160,36 @@ private fun fieldStatus( } ?: FieldStatus.Valid } else if (field.optional == true) FieldStatus.Valid else FieldStatus.Blank +} + +@Preview +@Composable +fun SelectUserAttributesScreenPreview() { + SelectUserAttributesScreen( + viewModel = FakeReducerViewModel( + state = ReducerState.Backup( + backupState = BackupStates.UserAttributesCollecting, + identityAttributes = mapOf( + "full_name" to "Max Musterman", + "birthdate" to "2000-01-01", + ), + requiredAttributes = listOf( + UserAttributeSpec( + type = "string", + name = "full_name", + label = "Full name", + widget = "anastasis_gtk_ia_full_name", + uuid = "9e8f463f-575f-42cb-85f3-759559997331", + ), + UserAttributeSpec( + type = "date", + name = "birthdate", + label = "Birthdate", + widget = "anastasis_gtk_ia_birthdate", + uuid = "83d655c7-bdb6-484d-904e-80c1058c8854", + ), + ), + ), + ), + ) } \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt index a7c3f62..6437e5a 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/dialogs/EditMethodDialog.kt @@ -26,11 +26,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import net.taler.anastasis.R -import net.taler.anastasis.shared.Utils import net.taler.anastasis.models.AuthMethod import net.taler.anastasis.ui.forms.EditEmailForm import net.taler.anastasis.ui.forms.EditQuestionForm import net.taler.anastasis.ui.forms.EditSmsForm +import net.taler.common.CryptoUtils @Composable fun EditMethodDialog( @@ -40,7 +40,7 @@ fun EditMethodDialog( onCancel: () -> Unit, ) { var localMethod by remember { mutableStateOf(method?.copy( - challenge = Utils.decodeBase32(method.challenge), + challenge = CryptoUtils.decodeCrock(method.challenge).toString(Charsets.UTF_8), )) } AlertDialog( onDismissRequest = onCancel, @@ -73,7 +73,9 @@ fun EditMethodDialog( TextButton(onClick = { localMethod?.let { onMethodEdited( it.copy( - challenge = Utils.encodeBase32(it.challenge) + challenge = CryptoUtils.encodeCrock( + it.challenge.toByteArray(Charsets.UTF_8), + ) ) ) } }) { diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt new file mode 100644 index 0000000..2c97e92 --- /dev/null +++ b/anastasis/src/main/java/net/taler/anastasis/ui/forms/EditSecretForm.kt @@ -0,0 +1,85 @@ +/* + * This file is part of GNU Taler + * (C) 2023 Taler Systems S.A. + * + * GNU Taler 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, or (at your option) any later version. + * + * GNU Taler 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 + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.anastasis.ui.forms + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import net.taler.anastasis.R +import net.taler.anastasis.ui.theme.LocalSpacing +import net.taler.common.Timestamp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EditSecretForm( + modifier: Modifier = Modifier, + name: String, + value: String, + expiration: Timestamp? = null, + onSecretEdited: ( + name: String, + value: String, + expiration: Timestamp?, + ) -> Unit, +) { + val focusRequester2 = remember { FocusRequester() } + + Column( + modifier = modifier, + ) { + OutlinedTextField( + modifier = Modifier + .padding( + start = LocalSpacing.current.medium, + end = LocalSpacing.current.medium, + bottom = LocalSpacing.current.small, + ).fillMaxWidth(), + value = name, + maxLines = 1, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + keyboardActions = KeyboardActions(onNext = { focusRequester2.requestFocus() }), + onValueChange = { onSecretEdited(it, value, expiration) }, + label = { Text(stringResource(R.string.secret_name)) }, + supportingText = { Text(stringResource(R.string.secret_unique)) }, + ) + + OutlinedTextField( + modifier = Modifier + .focusRequester(focusRequester2) + .padding( + start = LocalSpacing.current.medium, + end = LocalSpacing.current.medium, + bottom = LocalSpacing.current.small, + ).fillMaxWidth(), + value = value, + onValueChange = { onSecretEdited(name, it, expiration) }, + label = { Text(stringResource(R.string.secret_text)) }, + ) + } +} \ No newline at end of file diff --git a/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt b/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt index cf24c48..8cee602 100644 --- a/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt +++ b/anastasis/src/main/java/net/taler/anastasis/ui/home/HomeScreen.kt @@ -51,7 +51,7 @@ fun HomeScreen( icon = { Icon(Icons.Outlined.Upload, null) }, headline = stringResource(R.string.backup_secret), onClick = { - viewModel.reducerManager.startBackup() + viewModel.reducerManager?.startBackup() }, ) @@ -64,7 +64,7 @@ fun HomeScreen( icon = { Icon(Icons.Outlined.Download, null) }, headline = stringResource(R.string.recover_secret), onClick = { - viewModel.reducerManager.startRecovery() + viewModel.reducerManager?.startRecovery() }, ) diff --git a/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt index 46be209..ad90528 100644 --- a/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt +++ b/anastasis/src/main/java/net/taler/anastasis/viewmodels/ReducerViewModel.kt @@ -20,6 +20,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import net.taler.anastasis.Routes @@ -31,15 +32,25 @@ import net.taler.anastasis.models.ReducerState import net.taler.anastasis.reducers.ReducerManager import javax.inject.Inject +interface ReducerViewModelI { + val reducerManager: ReducerManager? + val reducerState: StateFlow + val reducerError: StateFlow + + fun goBack(): Boolean + fun goHome() + fun cleanError() +} + @HiltViewModel -class ReducerViewModel @Inject constructor(): ViewModel() { +class ReducerViewModel @Inject constructor(): ViewModel(), ReducerViewModelI { private val api = AnastasisReducerApi() - val reducerManager: ReducerManager + override val reducerManager: ReducerManager? private val _reducerState = MutableStateFlow(null) - val reducerState = _reducerState.asStateFlow() + override val reducerState = _reducerState.asStateFlow() private val _reducerError = MutableStateFlow(null) - val reducerError = _reducerError.asStateFlow() + override val reducerError = _reducerError.asStateFlow() private val _navRoute = MutableStateFlow(Routes.Home.route) val navRoute = _navRoute.asStateFlow() @@ -57,11 +68,11 @@ class ReducerViewModel @Inject constructor(): ViewModel() { reducerManager.startSyncingProviders() Routes.SelectAuthMethods.route } - BackupStates.PoliciesReviewing -> Routes.ReviewPoliciesScreen.route - BackupStates.SecretEditing -> TODO() + BackupStates.PoliciesReviewing -> Routes.ReviewPolicies.route + BackupStates.SecretEditing -> Routes.EditSecret.route BackupStates.TruthsPaying -> TODO() BackupStates.PoliciesPaying -> TODO() - BackupStates.BackupFinished -> TODO() + BackupStates.BackupFinished -> Routes.BackupFinished.route } is ReducerState.Recovery -> when (it.recoveryState) { RecoveryStates.ContinentSelecting -> Routes.SelectContinent.route @@ -79,14 +90,14 @@ class ReducerViewModel @Inject constructor(): ViewModel() { } } - fun goBack(): Boolean = when (val state = reducerState.value) { + override fun goBack(): Boolean = when (val state = reducerState.value) { is ReducerState.Backup -> when (state.backupState) { BackupStates.ContinentSelecting -> { goHome() false } else -> { - reducerManager.back() + reducerManager?.back() false } } @@ -96,22 +107,43 @@ class ReducerViewModel @Inject constructor(): ViewModel() { false } else -> { - reducerManager.back() + reducerManager?.back() false } } is ReducerState.Error -> { - reducerManager.back() + reducerManager?.back() false } else -> true } - fun goHome() { + override fun goHome() { + _reducerState.value = null + } + + override fun cleanError() { + _reducerError.value = null + } +} + +class FakeReducerViewModel( + state: ReducerState, + error: TalerErrorInfo? = null, +): ReducerViewModelI { + override val reducerManager = null + private val _reducerState = MutableStateFlow(state) + override val reducerState: StateFlow = _reducerState.asStateFlow() + private val _reducerError = MutableStateFlow(error) + override val reducerError: StateFlow = _reducerError.asStateFlow() + + override fun goBack(): Boolean = false + + override fun goHome() { _reducerState.value = null } - fun cleanError() { + override fun cleanError() { _reducerError.value = null } } \ No newline at end of file diff --git a/anastasis/src/main/res/values/strings.xml b/anastasis/src/main/res/values/strings.xml index bd903c1..5a6aecf 100644 --- a/anastasis/src/main/res/values/strings.xml +++ b/anastasis/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ Provided by %1$s Disabled Unknown + Success Error This field is required This field is invalid @@ -41,6 +42,7 @@ Authentication methods Recovery policies Provide secret to backup + Your backup is complete! Add a question challenge Add a SMS challenge Add an e-mail challenge @@ -62,4 +64,6 @@ Secret name This should be unique Secret text + Your backup is being stored by the following providers: + Version %1$d expires at %2$s \ No newline at end of file diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt index 4b77f85..c0c28fb 100644 --- a/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt +++ b/taler-kotlin-android/src/main/java/net/taler/common/Bech32.kt @@ -105,7 +105,7 @@ class Bech32 { fun generateFakeSegwitAddress(reservePub: String?, addr: String): List { if (reservePub == null || reservePub.isEmpty()) return listOf() - val pub = CyptoUtils.decodeCrock(reservePub) + val pub = CryptoUtils.decodeCrock(reservePub) if (pub.size != 32) return listOf() val firstRnd = pub.copyOfRange(0, 4) diff --git a/taler-kotlin-android/src/main/java/net/taler/common/CryptoUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/CryptoUtils.kt new file mode 100644 index 0000000..0dba7aa --- /dev/null +++ b/taler-kotlin-android/src/main/java/net/taler/common/CryptoUtils.kt @@ -0,0 +1,97 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler 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, or (at your option) any later version. + * + * GNU Taler 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 + * GNU Taler; see the file COPYING. If not, see + */ + +package net.taler.common + +import kotlin.math.floor + +object CryptoUtils { + private const val encTable = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + + internal fun getValue(c: Char): Int { + val a = when (c) { + 'o','O' -> '0' + 'i','I','l','L' -> '1' + 'u','U' -> 'V' + else -> c + } + if (a in '0'..'9') { + return a - '0' + } + val A = if (a in 'a'..'z') a.uppercaseChar() else a + var dec = 0 + if (A in 'A'..'Z') { + if ('I' < A) dec++ + if ('L' < A) dec++ + if ('O' < A) dec++ + if ('U' < A) dec++ + return A - 'A' + 10 - dec + } + throw Error("encoding error") + } + + fun encodeCrock(bytes: ByteArray): String { + var sb = "" + val size = bytes.size + var bitBuf = 0 + var numBits = 0 + var pos = 0 + while (pos < size || numBits > 0) { + if (pos < size && numBits < 5) { + val d = bytes[pos++] + bitBuf = bitBuf.shl(8).or(d.toInt()) + numBits += 8 + } + if (numBits < 5) { + // zero-padding + bitBuf = bitBuf.shl(5 - numBits) + numBits = 5 + } + val v = bitBuf.ushr(numBits - 5).and(31) + sb += encTable[v] + numBits -= 5 + } + return sb + } + + fun decodeCrock(e: String): ByteArray { + val size = e.length + var bitpos = 0 + var bitbuf = 0 + var readPosition = 0 + val outLen = floor((size * 5f) / 8).toInt() + val out = ByteArray(outLen) + var outPos = 0 + while (readPosition < size || bitpos > 0) { + if (readPosition < size) { + val v = getValue(e[readPosition++]) + bitbuf = bitbuf.shl(5).or(v) + bitpos += 5 + } + while (bitpos >= 8) { + val d = bitbuf.shr(bitpos -8).and(0xff).toByte() + out[outPos++] = d + bitpos -= 8 + } + if (readPosition == size && bitpos > 0) { + bitbuf = bitbuf.shl( 8 - bitpos).and(0xff) + bitpos = if (bitbuf == 0) 0 else 8 + } + } + return out + } + +} \ No newline at end of file diff --git a/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt b/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt deleted file mode 100644 index c1fbe8c..0000000 --- a/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * This file is part of GNU Taler - * (C) 2022 Taler Systems S.A. - * - * GNU Taler 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, or (at your option) any later version. - * - * GNU Taler 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 - * GNU Taler; see the file COPYING. If not, see - */ - -package net.taler.common - -import kotlin.math.floor - -object CyptoUtils { - internal fun getValue(c: Char): Int { - val a = when (c) { - 'o','O' -> '0' - 'i','I','l','L' -> '1' - 'u','U' -> 'V' - else -> c - } - if (a in '0'..'9') { - return a - '0' - } - val A = if (a in 'a'..'z') a.uppercaseChar() else a - var dec = 0 - if (A in 'A'..'Z') { - if ('I' < A) dec++ - if ('L' < A) dec++ - if ('O' < A) dec++ - if ('U' < A) dec++ - return A - 'A' + 10 - dec - } - throw Error("encoding error") - } - - fun decodeCrock(e: String): ByteArray { - val size = e.length - var bitpos = 0 - var bitbuf = 0 - var readPosition = 0 - val outLen = floor((size * 5f) / 8).toInt() - val out = ByteArray(outLen) - var outPos = 0 - while (readPosition < size || bitpos > 0) { - if (readPosition < size) { - val v = getValue(e[readPosition++]) - bitbuf = bitbuf.shl(5).or(v) - bitpos += 5 - } - while (bitpos >= 8) { - val d = bitbuf.shr(bitpos -8).and(0xff).toByte() - out[outPos++] = d - bitpos -= 8 - } - if (readPosition == size && bitpos > 0) { - bitbuf = bitbuf.shl( 8 - bitpos).and(0xff) - bitpos = if (bitbuf == 0) 0 else 8 - } - } - return out - } - -} \ No newline at end of file -- cgit v1.2.3