1package com.peachpasswords.vault23import android.content.Context4import dagger.hilt.android.qualifiers.ApplicationContext5import kotlinx.serialization.Serializable6import kotlinx.serialization.decodeFromString7import kotlinx.serialization.encodeToString8import kotlinx.serialization.json.Json9import javax.inject.Inject10import javax.inject.Singleton111213141516171819@Serializable20data class StoredVault(21 val version: Int = 1,22 val salt: String,23 val blob: String,24 val recoveryEscrow: RecoveryEscrow? = null,25 val rootKeyEscrow: RootKeyEscrow? = null,26)272829303132333435363738394041@Serializable42data class RootKeyEscrow(43 val salt: String,44 val wrappedRootKey: String,45 val nonce: String,46 val tag: String,47)4849505152535455565758@Serializable59data class RecoveryEscrow(60 val salt: String,61 val blob: String,62)636465@Serializable66data class Tombstone(val id: String, val deletedAt: Long)67686970717273747576777879@Serializable80data class VaultPayload(81 val entries: List<VaultEntry> = emptyList(),82 val duressConfigured: Boolean = false,83 val rootKeyB64: String = "",84 85868788 val recoveryWordHash: String? = null,89 90 val recoveryWordsSet: Boolean = false,91)929394object VaultCodec {95 private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }96 fun encode(payload: VaultPayload): ByteArray = json.encodeToString(payload).toByteArray(Charsets.UTF_8)97 fun decode(bytes: ByteArray): VaultPayload = json.decodeFromString(String(bytes, Charsets.UTF_8))98 fun encodeSettings(s: VaultSettings): String = json.encodeToString(s)99 fun decodeSettings(s: String): VaultSettings? = runCatching { json.decodeFromString<VaultSettings>(s) }.getOrNull()100 fun encodeSlot(s: StoredVault): String = json.encodeToString(s)101 fun decodeSlot(s: String): StoredVault? = runCatching { json.decodeFromString<StoredVault>(s) }.getOrNull()102}103104105106107108109interface VaultStorage {110 fun loadSlotA(): StoredVault?111 fun hasVault(): Boolean112 fun writeSlotA(slot: StoredVault): Boolean113 fun writeSlotB(blobBase64: String): Boolean114 fun loadDecoySlot(): StoredVault?115 fun writeDecoySlot(slot: StoredVault): Boolean116 fun hasDecoySlot(): Boolean117 fun clearDecoySlot()118 fun version(): Long119 fun stampLocalMutation(): Long120 fun loadSettings(): VaultSettings121 fun writeSettings(settings: VaultSettings): Boolean122 fun clearVault()123}124125126127128129130131132@Singleton133class VaultStore @Inject constructor(134 @ApplicationContext context: Context,135) : VaultStorage {136 private val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)137138 override fun loadSlotA(): StoredVault? = prefs.getString(KEY_SLOT_A, null)?.let(VaultCodec::decodeSlot)139140 override fun hasVault(): Boolean = prefs.contains(KEY_SLOT_A)141142 override fun writeSlotA(slot: StoredVault): Boolean =143 prefs.edit().putString(KEY_SLOT_A, VaultCodec.encodeSlot(slot)).commit()144145 override fun writeSlotB(blobBase64: String): Boolean =146 prefs.edit().putString(KEY_SLOT_B, blobBase64).commit()147148 override fun loadDecoySlot(): StoredVault? = prefs.getString(KEY_SLOT_DURESS, null)?.let(VaultCodec::decodeSlot)149 override fun writeDecoySlot(slot: StoredVault): Boolean =150 prefs.edit().putString(KEY_SLOT_DURESS, VaultCodec.encodeSlot(slot)).commit()151 override fun hasDecoySlot(): Boolean = prefs.contains(KEY_SLOT_DURESS)152 override fun clearDecoySlot() { prefs.edit().remove(KEY_SLOT_DURESS).commit() }153154 override fun version(): Long = prefs.getString(KEY_VERSION, null)?.toLongOrNull() ?: 0L155156 override fun stampLocalMutation(): Long {157 val next = maxOf(System.currentTimeMillis(), version() + 1L)158 prefs.edit().putString(KEY_VERSION, next.toString()).commit()159 return next160 }161162 override fun loadSettings(): VaultSettings =163 prefs.getString(KEY_SETTINGS, null)?.let(VaultCodec::decodeSettings) ?: VaultSettings()164165 override fun writeSettings(settings: VaultSettings): Boolean =166 prefs.edit().putString(KEY_SETTINGS, VaultCodec.encodeSettings(settings)).commit()167168 override fun clearVault() {169 prefs.edit()170 .remove(KEY_SLOT_A).remove(KEY_SLOT_B).remove(KEY_VERSION).remove(KEY_SETTINGS)171 .commit()172 }173174 companion object {175 private const val PREFS = "peach_vault_store"176 private const val KEY_SLOT_A = "peach_vault_a"177 private const val KEY_SLOT_B = "peach_vault_b"178 private const val KEY_SLOT_DURESS = "peach_vault_duress"179 private const val KEY_VERSION = "peach_last_modified_at"180 private const val KEY_SETTINGS = "peach_settings"181 }182}