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)6364@Serializable65data class VaultHistory(66 val snapshots: List<VaultSnapshot> = emptyList(),67)686970@Serializable71data class Tombstone(val id: String, val deletedAt: Long)72737475767778798081828384@Serializable85data class VaultPayload(86 val entries: List<VaultEntry> = emptyList(),87 val duressConfigured: Boolean = false,88 val rootKeyB64: String = "",89 90919293 val recoveryWordHash: String? = null,94 95 val recoveryWordsSet: Boolean = false,96)979899object VaultCodec {100 private val json = Json { ignoreUnknownKeys = true; encodeDefaults = true }101102 103 104 105 106 107 fun encode(payload: VaultPayload): ByteArray =108 VaultCrypto.padToBucket(json.encodeToString(payload).toByteArray(Charsets.UTF_8))109 fun decode(bytes: ByteArray): VaultPayload =110 json.decodeFromString(String(bytes, Charsets.UTF_8).trimEnd('\u0000'))111 112 internal fun decodeEntryForTest(entryJson: String): VaultEntry = json.decodeFromString(entryJson)113 internal fun encodeEntryForTest(entry: VaultEntry): String = json.encodeToString(entry)114115 fun encodeSettings(s: VaultSettings): String = json.encodeToString(s)116 fun decodeSettings(s: String): VaultSettings? = runCatching { json.decodeFromString<VaultSettings>(s) }.getOrNull()117 fun encodeSlot(s: StoredVault): String = json.encodeToString(s)118 fun decodeSlot(s: String): StoredVault? = runCatching { json.decodeFromString<StoredVault>(s) }.getOrNull()119 fun encodeHistory(h: VaultHistory): String = json.encodeToString(h)120 fun decodeHistory(s: String): VaultHistory? = runCatching { json.decodeFromString<VaultHistory>(s) }.getOrNull()121}122123124125126127128interface VaultStorage {129 fun loadSlotA(): StoredVault?130 fun hasVault(): Boolean131 fun writeSlotA(slot: StoredVault): Boolean132 fun loadSlotB(): String? = null133 fun writeSlotB(blobBase64: String): Boolean134 fun loadDecoySlot(): StoredVault?135 fun writeDecoySlot(slot: StoredVault): Boolean136 fun hasDecoySlot(): Boolean137 fun clearDecoySlot()138 fun version(): Long139 fun stampLocalMutation(): Long140 fun loadSettings(): VaultSettings141 fun writeSettings(settings: VaultSettings): Boolean142 fun loadVaultHistory(): VaultHistory = VaultHistory()143 fun writeVaultHistory(history: VaultHistory): Boolean = true144 fun clearVaultHistory() {}145 fun clearVault()146}147148149150151152153154155@Singleton156class VaultStore @Inject constructor(157 @ApplicationContext context: Context,158) : VaultStorage {159 private val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)160161 override fun loadSlotA(): StoredVault? = prefs.getString(KEY_SLOT_A, null)?.let(VaultCodec::decodeSlot)162163 override fun hasVault(): Boolean = prefs.contains(KEY_SLOT_A)164165 override fun writeSlotA(slot: StoredVault): Boolean =166 prefs.edit().putString(KEY_SLOT_A, VaultCodec.encodeSlot(slot)).commit()167168 override fun loadSlotB(): String? = prefs.getString(KEY_SLOT_B, null)169170 override fun writeSlotB(blobBase64: String): Boolean =171 prefs.edit().putString(KEY_SLOT_B, blobBase64).commit()172173 override fun loadDecoySlot(): StoredVault? = prefs.getString(KEY_SLOT_DURESS, null)?.let(VaultCodec::decodeSlot)174 override fun writeDecoySlot(slot: StoredVault): Boolean =175 prefs.edit().putString(KEY_SLOT_DURESS, VaultCodec.encodeSlot(slot)).commit()176 override fun hasDecoySlot(): Boolean = prefs.contains(KEY_SLOT_DURESS)177 override fun clearDecoySlot() { prefs.edit().remove(KEY_SLOT_DURESS).commit() }178179 override fun version(): Long = prefs.getString(KEY_VERSION, null)?.toLongOrNull() ?: 0L180181 override fun stampLocalMutation(): Long {182 val next = maxOf(System.currentTimeMillis(), version() + 1L)183 prefs.edit().putString(KEY_VERSION, next.toString()).commit()184 return next185 }186187 override fun loadSettings(): VaultSettings =188 prefs.getString(KEY_SETTINGS, null)?.let(VaultCodec::decodeSettings) ?: VaultSettings()189190 override fun writeSettings(settings: VaultSettings): Boolean =191 prefs.edit().putString(KEY_SETTINGS, VaultCodec.encodeSettings(settings)).commit()192193 override fun loadVaultHistory(): VaultHistory =194 prefs.getString(KEY_HISTORY, null)?.let(VaultCodec::decodeHistory) ?: VaultHistory()195196 override fun writeVaultHistory(history: VaultHistory): Boolean =197 prefs.edit().putString(KEY_HISTORY, VaultCodec.encodeHistory(history)).commit()198199 override fun clearVaultHistory() {200 prefs.edit().remove(KEY_HISTORY).commit()201 }202203 override fun clearVault() {204 prefs.edit()205 .remove(KEY_SLOT_A).remove(KEY_SLOT_B).remove(KEY_VERSION).remove(KEY_SETTINGS).remove(KEY_HISTORY)206 .commit()207 }208209 companion object {210 private const val PREFS = "peach_vault_store"211 private const val KEY_SLOT_A = "peach_vault_a"212 private const val KEY_SLOT_B = "peach_vault_b"213 private const val KEY_SLOT_DURESS = "peach_vault_duress"214 private const val KEY_VERSION = "peach_last_modified_at"215 private const val KEY_SETTINGS = "peach_settings"216 private const val KEY_HISTORY = "peach_vault_history"217 }218}