@@ -75,9 +75,15 @@ import com.artemchep.keyguard.common.model.creditCards
7575import com.artemchep.keyguard.common.model.fileName
7676import com.artemchep.keyguard.common.model.fileSize
7777import com.artemchep.keyguard.common.model.titleH
78+ import com.artemchep.keyguard.common.service.crypto.SshKeyImportError
79+ import com.artemchep.keyguard.common.service.crypto.SshKeyImportRequest
80+ import com.artemchep.keyguard.common.service.crypto.SshKeyImportResult
81+ import com.artemchep.keyguard.common.service.crypto.SshKeyImportService
7882import com.artemchep.keyguard.common.service.clipboard.ClipboardService
7983import com.artemchep.keyguard.common.service.googleauthenticator.OtpMigrationService
8084import com.artemchep.keyguard.common.service.logging.LogRepository
85+ import com.artemchep.keyguard.common.service.text.TextService
86+ import com.artemchep.keyguard.common.service.text.readFromFileAsText
8187import com.artemchep.keyguard.common.usecase.AddCipher
8288import com.artemchep.keyguard.common.usecase.CipherUnsecureUrlCheck
8389import com.artemchep.keyguard.common.usecase.CopyText
@@ -169,6 +175,7 @@ import kotlinx.coroutines.flow.onStart
169175import kotlinx.coroutines.flow.scan
170176import kotlinx.coroutines.flow.shareIn
171177import kotlinx.coroutines.flow.update
178+ import kotlinx.coroutines.launch
172179import kotlin.time.Clock
173180import kotlin.time.Instant
174181import kotlinx.serialization.SerialName
@@ -194,6 +201,8 @@ fun produceAddScreenState(
194201 getTotpCode = instance(),
195202 getGravatarUrl = instance(),
196203 getMarkdown = instance(),
204+ textService = instance(),
205+ sshKeyImportService = instance(),
197206 logRepository = instance(),
198207 clipboardService = instance(),
199208 otpMigrationService = instance(),
@@ -225,6 +234,8 @@ fun produceAddScreenState(
225234 getTotpCode : GetTotpCode ,
226235 getGravatarUrl : GetGravatarUrl ,
227236 getMarkdown : GetMarkdown ,
237+ textService : TextService ,
238+ sshKeyImportService : SshKeyImportService ,
228239 logRepository : LogRepository ,
229240 clipboardService : ClipboardService ,
230241 otpMigrationService : OtpMigrationService ,
@@ -247,6 +258,7 @@ fun produceAddScreenState(
247258) {
248259 val copyText = copy(clipboardService)
249260 val markdown = getMarkdown().first()
261+ val filePickerEvents = EventFlow <FilePickerIntent <* >>()
250262
251263 val ownershipFlow = produceOwnershipFlow(
252264 args = args,
@@ -316,6 +328,10 @@ fun produceAddScreenState(
316328 )
317329 val sshKeyHolder = produceSshKeyState(
318330 args = args,
331+ textService = textService,
332+ sshKeyImportService = sshKeyImportService,
333+ showMessage = showMessage,
334+ filePickerIntentSink = filePickerEvents,
319335 )
320336
321337 val typeFlow = kotlin.run {
@@ -326,10 +342,8 @@ fun produceAddScreenState(
326342 flowOf(initialValue)
327343 }
328344
329- val filePickerIntentSink = EventFlow <FilePickerIntent <* >>()
330-
331345 val sideEffects = AddState .SideEffects (
332- filePickerIntentFlow = filePickerIntentSink ,
346+ filePickerIntentFlow = filePickerEvents ,
333347 )
334348
335349 val passkeysFactories = kotlin.run {
@@ -485,7 +499,7 @@ fun produceAddScreenState(
485499 add(" attachment" , model)
486500 }
487501 }
488- filePickerIntentSink .emit(intent)
502+ filePickerEvents .emit(intent)
489503 },
490504 )
491505 val item = AddStateItem .Add (
@@ -3409,10 +3423,15 @@ data class KeyPairDecor2(
34093423data class KeyPairDecor2Brr (
34103424 val keyPair : KeyPairDecor2 ? = null ,
34113425 val onChange : (KeyPair ) -> Unit ,
3426+ val onImport : () -> Unit ,
34123427)
34133428
34143429private suspend fun RememberStateFlowScope.produceSshKeyState (
34153430 args : AddRoute .Args ,
3431+ textService : TextService ,
3432+ sshKeyImportService : SshKeyImportService ,
3433+ showMessage : ShowMessage ,
3434+ filePickerIntentSink : EventFlow <FilePickerIntent <* >>,
34163435): TmpSshKey {
34173436 val prefix = " sshKey"
34183437
@@ -3434,19 +3453,130 @@ private suspend fun RememberStateFlowScope.produceSshKeyState(
34343453 fingerprint = args.keyPair?.publicKey?.fingerprint ? : args.initialValue?.sshKey?.fingerprint ? : " " ,
34353454 )
34363455 }
3456+
3457+ suspend fun importKey (
3458+ fileName : String? ,
3459+ content : String ,
3460+ passphrase : String? ,
3461+ // callbacks
3462+ onNeedsPassphrase : suspend (SshKeyImportResult .NeedsPassphrase ) -> Unit ,
3463+ ) = when (
3464+ val result = sshKeyImportService.import(
3465+ SshKeyImportRequest (
3466+ content = content,
3467+ fileName = fileName,
3468+ passphrase = passphrase,
3469+ ),
3470+ )
3471+ ) {
3472+ is SshKeyImportResult .Success -> {
3473+ val msg = ToastMessage (
3474+ type = ToastMessage .Type .SUCCESS ,
3475+ title = translate(Res .string.ssh_key_import_passphrase_title),
3476+ )
3477+ showMessage.copy(msg)
3478+ // success!
3479+ sink.value = result.keyPair.toDecor()
3480+ }
3481+
3482+ is SshKeyImportResult .NeedsPassphrase -> {
3483+ // Redirect to a next flow or
3484+ // exit there!
3485+ onNeedsPassphrase(result)
3486+ }
3487+
3488+ is SshKeyImportResult .Error -> {
3489+ val msg = createLocalizedSshKeyImportErrorToast(result.reason)
3490+ showMessage.copy(msg)
3491+ }
3492+ }
3493+
3494+ suspend fun onImportKeyWithPassphrase (
3495+ result : SshKeyImportResult .NeedsPassphrase ,
3496+ fileName : String? ,
3497+ content : String ,
3498+ ) {
3499+ val passphraseTitle = translate(Res .string.ssh_key_import_passphrase_title)
3500+ val passphraseHint = translate(Res .string.ssh_key_import_passphrase_hint)
3501+
3502+ val intent = createConfirmationDialogIntent(
3503+ item = ConfirmationRoute .Args .Item .StringItem (
3504+ key = " $id .passphrase" ,
3505+ title = passphraseTitle,
3506+ hint = passphraseHint,
3507+ type = ConfirmationRoute .Args .Item .StringItem .Type .Password ,
3508+ canBeEmpty = false ,
3509+ ),
3510+ title = translate(Res .string.ssh_key_import_passphrase_dialog_title),
3511+ message = translate(
3512+ Res .string.ssh_key_import_passphrase_dialog_message,
3513+ result.formatLabel,
3514+ ),
3515+ ) { passphrase ->
3516+ appScope.launch {
3517+ importKey(
3518+ content = content,
3519+ fileName = fileName,
3520+ passphrase = passphrase,
3521+ // callbacks
3522+ onNeedsPassphrase = {
3523+ // show error message
3524+ val msg = createLocalizedSshKeyImportPassphraseErrorToast()
3525+ showMessage.copy(msg)
3526+ },
3527+ )
3528+ }
3529+ }
3530+ navigate(intent)
3531+ }
3532+
3533+ fun onImportKey () {
3534+ val intent = FilePickerIntent .OpenDocument (
3535+ mimeTypes = FilePickerIntent .mimeTypesAll,
3536+ ) { info ->
3537+ if (info == null ) {
3538+ return @OpenDocument
3539+ }
3540+
3541+ appScope.launch {
3542+ val content = kotlin.runCatching {
3543+ val uri = info.uri.toString()
3544+ textService.readFromFileAsText(uri)
3545+ }.getOrElse {
3546+ val msg = createLocalizedSshKeyImportReadErrorToast()
3547+ showMessage.copy(msg)
3548+ return @launch
3549+ }
3550+
3551+ val fileName = info.name
3552+ importKey(
3553+ content = content,
3554+ fileName = fileName,
3555+ passphrase = null ,
3556+ // callbacks
3557+ onNeedsPassphrase = { result ->
3558+ // redirect to a next flow
3559+ onImportKeyWithPassphrase(
3560+ result = result,
3561+ fileName = fileName,
3562+ content = content,
3563+ )
3564+ },
3565+ )
3566+ }
3567+ }
3568+ filePickerIntentSink.emit(intent)
3569+ }
3570+
34373571 val stateItem = LocalStateItem <KeyPairDecor2Brr , CreateRequest >(
34383572 flow = sink
34393573 .map { value ->
34403574 KeyPairDecor2Brr (
34413575 keyPair = value,
34423576 onChange = {
3443- val new = KeyPairDecor2 (
3444- privateKey = it.privateKey.ssh,
3445- publicKey = it.publicKey.ssh,
3446- fingerprint = it.publicKey.fingerprint,
3447- )
3448- sink.value = new
3577+ sink.value = it.toDecor()
34493578 },
3579+ onImport = ::onImportKey,
34503580 )
34513581 }
34523582 .persistingStateIn(
@@ -3456,6 +3586,7 @@ private suspend fun RememberStateFlowScope.produceSshKeyState(
34563586 onChange = {
34573587 // Do nothing
34583588 },
3589+ onImport = :: onImportKey,
34593590 ),
34603591 ),
34613592 populator = { state ->
@@ -3491,6 +3622,52 @@ private suspend fun RememberStateFlowScope.produceSshKeyState(
34913622 )
34923623}
34933624
3625+ suspend fun TranslatorScope.createLocalizedSshKeyImportErrorToast (
3626+ reason : SshKeyImportError ,
3627+ ): ToastMessage = when (reason) {
3628+ SshKeyImportError .UnsupportedFormat -> createSshKeyImportToast(
3629+ title = translate(Res .string.ssh_key_import_failed_title),
3630+ text = translate(Res .string.ssh_key_import_error_unsupported_format),
3631+ )
3632+ SshKeyImportError .UnsupportedAlgorithm -> createSshKeyImportToast(
3633+ title = translate(Res .string.ssh_key_import_failed_title),
3634+ text = translate(Res .string.ssh_key_import_error_unsupported_algorithm),
3635+ )
3636+ SshKeyImportError .InvalidPassphrase -> createSshKeyImportToast(
3637+ title = translate(Res .string.ssh_key_import_failed_title),
3638+ text = translate(Res .string.ssh_key_import_error_invalid_passphrase),
3639+ )
3640+ SshKeyImportError .MalformedKey -> createSshKeyImportToast(
3641+ title = translate(Res .string.ssh_key_import_failed_title),
3642+ text = translate(Res .string.ssh_key_import_error_malformed_key),
3643+ )
3644+ }
3645+
3646+ suspend fun TranslatorScope.createLocalizedSshKeyImportReadErrorToast (): ToastMessage = createSshKeyImportToast(
3647+ title = translate(Res .string.ssh_key_import_failed_title),
3648+ text = translate(Res .string.ssh_key_import_error_read),
3649+ )
3650+
3651+ suspend fun TranslatorScope.createLocalizedSshKeyImportPassphraseErrorToast (): ToastMessage = createSshKeyImportToast(
3652+ title = translate(Res .string.ssh_key_import_failed_title),
3653+ text = translate(Res .string.ssh_key_import_error_passphrase_required),
3654+ )
3655+
3656+ private fun createSshKeyImportToast (
3657+ title : String = "Failed to import SSH key",
3658+ text : String ,
3659+ ): ToastMessage = ToastMessage (
3660+ type = ToastMessage .Type .ERROR ,
3661+ title = title,
3662+ text = text,
3663+ )
3664+
3665+ private fun KeyPair.toDecor () = KeyPairDecor2 (
3666+ privateKey = privateKey.ssh,
3667+ publicKey = publicKey.ssh,
3668+ fingerprint = publicKey.fingerprint,
3669+ )
3670+
34943671suspend fun <Request > RememberStateFlowScope.createItem (
34953672 prefix : String ,
34963673 key : String ,
0 commit comments