1package com.peachpasswords.ui.codex23import android.content.Context4import android.net.Uri5import androidx.lifecycle.ViewModel6import androidx.lifecycle.viewModelScope7import androidx.camera.core.ImageProxy8import com.peachpasswords.codex.ChunkEnvelope9import com.peachpasswords.codex.CodexAztecFrame10import com.peachpasswords.codex.CodexException11import com.peachpasswords.codex.CodexGenerator12import com.peachpasswords.codex.CodexManifest13import com.peachpasswords.codex.CodexPayload14import com.peachpasswords.codex.CodexRestore15import com.peachpasswords.codex.CodexStep16import com.peachpasswords.vault.UnlockResult17import com.peachpasswords.vault.VaultRepository18import dagger.hilt.android.lifecycle.HiltViewModel19import dagger.hilt.android.qualifiers.ApplicationContext20import kotlinx.coroutines.Dispatchers21import kotlinx.coroutines.flow.MutableStateFlow22import kotlinx.coroutines.flow.StateFlow23import kotlinx.coroutines.flow.asStateFlow24import kotlinx.coroutines.launch25import kotlinx.coroutines.withContext26import javax.inject.Inject2728sealed interface GenerateState {29 data object Idle : GenerateState30 data class Running(val step: CodexStep, val current: Int, val total: Int) : GenerateState31 data class Done(val manifest: CodexManifest, val pdf: ByteArray) : GenerateState32 data class Error(val message: String) : GenerateState33}3435sealed interface RestoreState {36 data object Idle : RestoreState37 data class Scanning(val page: Int, val pages: Int) : RestoreState38 data class Decoded(val payload: CodexPayload, val codesFound: Int) : RestoreState39 data object Restoring : RestoreState40 data object Restored : RestoreState41 data class Error(val message: String) : RestoreState42}43444546474849sealed interface CameraScanState {50 data object Choosing : CameraScanState51 data class Scanning(val codesFound: Int) : CameraScanState52 data class Timeout(val codesFound: Int) : CameraScanState53}5455@HiltViewModel56class CodexViewModel @Inject constructor(57 @ApplicationContext private val context: Context,58 private val vault: VaultRepository,59) : ViewModel() {6061 private val _generate = MutableStateFlow<GenerateState>(GenerateState.Idle)62 val generate: StateFlow<GenerateState> = _generate.asStateFlow()6364 65 val recoveryWordsSet: Boolean get() = vault.isRecoveryWordsSet()6667 private val recoveryWords: List<String> by lazy {68 vault.syncMasterKey()?.let { k ->69 try { com.peachpasswords.vault.rootKeyToWords(k) }70 finally { com.peachpasswords.vault.VaultCrypto.zero(k) }71 } ?: emptyList()72 }7374 75 fun gateMatches(w3: String, w7: String, w11: String): Boolean {76 val ws = recoveryWords77 if (ws.size < 11) return false78 return w3.trim().lowercase() == ws[2] &&79 w7.trim().lowercase() == ws[6] &&80 w11.trim().lowercase() == ws[10]81 }8283 private val _restore = MutableStateFlow<RestoreState>(RestoreState.Idle)84 val restore: StateFlow<RestoreState> = _restore.asStateFlow()8586 fun startGenerate(now: Long) {87 _generate.value = GenerateState.Running(CodexStep.COMPRESS, 0, 1)88 viewModelScope.launch {89 try {90 val (pdf, manifest) = withContext(Dispatchers.Default) {91 CodexGenerator.generate(vault, now) { step, cur, total ->92 _generate.value = GenerateState.Running(step, cur, total)93 }94 }95 _generate.value = GenerateState.Done(manifest, pdf)96 } catch (e: CodexException) {97 _generate.value = GenerateState.Error(e.message ?: e.code.name)98 } catch (e: Exception) {99 _generate.value = GenerateState.Error(e.message ?: "generation_failed")100 }101 }102 }103104 fun writePdf(uri: Uri): Boolean = try {105 val pdf = (_generate.value as? GenerateState.Done)?.pdf ?: return false106 context.contentResolver.openOutputStream(uri)?.use { it.write(pdf) } != null107 } catch (_: Exception) {108 false109 }110111 fun scan(uri: Uri) {112 _restore.value = RestoreState.Scanning(0, 0)113 viewModelScope.launch {114 try {115 val payload = withContext(Dispatchers.Default) {116 val result = CodexRestore.decodeFromUri(context, uri) { page, pages ->117 _restore.value = RestoreState.Scanning(page, pages)118 }119 if (result.envelopes.isEmpty()) {120 throw CodexException(121 com.peachpasswords.codex.CodexErrorCode.INSUFFICIENT_CHUNKS,122 "No Aztec codes could be read from this file",123 )124 }125 CodexRestore.reconstructPayload(result.envelopes) to result.envelopes.size126 }127 _restore.value = RestoreState.Decoded(payload.first, payload.second)128 } catch (e: CodexException) {129 _restore.value = RestoreState.Error(e.message ?: e.code.name)130 } catch (e: Exception) {131 _restore.value = RestoreState.Error(e.message ?: "restore_failed")132 }133 }134 }135136 fun confirmRestore(password: String, onDone: () -> Unit) {137 val payload = (_restore.value as? RestoreState.Decoded)?.payload ?: return138 _restore.value = RestoreState.Restoring139 viewModelScope.launch {140 when (val r = vault.restoreFromCodex(payload.vaultBlob, password)) {141 is UnlockResult.Success -> { _restore.value = RestoreState.Restored; onDone() }142 is UnlockResult.WrongPassword -> _restore.value = RestoreState.Error("Wrong master password")143 is UnlockResult.Error -> _restore.value = RestoreState.Error(r.reason)144 }145 }146 }147148 fun resetRestore() { _restore.value = RestoreState.Idle }149150 151 private val _camera = MutableStateFlow<CameraScanState>(CameraScanState.Choosing)152 val camera: StateFlow<CameraScanState> = _camera.asStateFlow()153154 155 private val cameraEnvelopes = LinkedHashMap<String, ChunkEnvelope>()156157 158 fun chooseRestore() {159 cameraEnvelopes.clear()160 _camera.value = CameraScanState.Choosing161 _restore.value = RestoreState.Idle162 }163164 165 fun startCameraScan() {166 cameraEnvelopes.clear()167 _camera.value = CameraScanState.Scanning(0)168 }169170 171 fun cameraTimedOut() {172 val s = _camera.value173 if (s is CameraScanState.Scanning) _camera.value = CameraScanState.Timeout(s.codesFound)174 }175176 177178179180181182 fun onCameraFrame(image: ImageProxy): Boolean {183 if (_camera.value !is CameraScanState.Scanning) return _restore.value is RestoreState.Decoded184 val text = CodexAztecFrame.decode(image) ?: return false185 val env = CodexRestore.parseEnvelope(text) ?: return false186 cameraEnvelopes.putIfAbsent("${env.s}:${env.i}", env)187 _camera.value = CameraScanState.Scanning(cameraEnvelopes.size)188 return try {189 val payload = CodexRestore.reconstructPayload(cameraEnvelopes.values.toList())190 _restore.value = RestoreState.Decoded(payload, cameraEnvelopes.size)191 true192 } catch (_: Exception) {193 false194 }195 }196}