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 fun encodeSettings(s: VaultSettings): String = json.encodeToString(s)112 fun decodeSettings(s: String): VaultSettings? = runCatching { json.decodeFromString<VaultSettings>(s) }.getOrNull()113 fun encodeSlot(s: StoredVault): String = json.encodeToString(s)114 fun decodeSlot(s: String): StoredVault? = runCatching { json.decodeFromString<StoredVault>(s) }.getOrNull()115 fun encodeHistory(h: VaultHistory): String = json.encodeToString(h)116 fun decodeHistory(s: String): VaultHistory? = runCatching { json.decodeFromString<VaultHistory>(s) }.getOrNull()117}118119120121122123124interface VaultStorage {125 fun loadSlotA(): StoredVault?126 fun hasVault(): Boolean127 fun writeSlotA(slot: StoredVault): Boolean128 fun loadSlotB(): String? = null129 fun writeSlotB(blobBase64: String): Boolean130 fun loadDecoySlot(): StoredVault?131 fun writeDecoySlot(slot: StoredVault): Boolean132 fun hasDecoySlot(): Boolean133 fun clearDecoySlot()134 fun version(): Long135 fun stampLocalMutation(): Long136 fun loadSettings(): VaultSettings137 fun writeSettings(settings: VaultSettings): Boolean138 fun loadVaultHistory(): VaultHistory = VaultHistory()139 fun writeVaultHistory(history: VaultHistory): Boolean = true140 fun clearVaultHistory() {}141 fun clearVault()142}143144145146147148149150151@Singleton152class VaultStore @Inject constructor(153 @ApplicationContext context: Context,154) : VaultStorage {155 private val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)156157 override fun loadSlotA(): StoredVault? = prefs.getString(KEY_SLOT_A, null)?.let(VaultCodec::decodeSlot)158159 override fun hasVault(): Boolean = prefs.contains(KEY_SLOT_A)160161 override fun writeSlotA(slot: StoredVault): Boolean =162 prefs.edit().putString(KEY_SLOT_A, VaultCodec.encodeSlot(slot)).commit()163164 override fun loadSlotB(): String? = prefs.getString(KEY_SLOT_B, null)165166 override fun writeSlotB(blobBase64: String): Boolean =167 prefs.edit().putString(KEY_SLOT_B, blobBase64).commit()168169 override fun loadDecoySlot(): StoredVault? = prefs.getString(KEY_SLOT_DURESS, null)?.let(VaultCodec::decodeSlot)170 override fun writeDecoySlot(slot: StoredVault): Boolean =171 prefs.edit().putString(KEY_SLOT_DURESS, VaultCodec.encodeSlot(slot)).commit()172 override fun hasDecoySlot(): Boolean = prefs.contains(KEY_SLOT_DURESS)173 override fun clearDecoySlot() { prefs.edit().remove(KEY_SLOT_DURESS).commit() }174175 override fun version(): Long = prefs.getString(KEY_VERSION, null)?.toLongOrNull() ?: 0L176177 override fun stampLocalMutation(): Long {178 val next = maxOf(System.currentTimeMillis(), version() + 1L)179 prefs.edit().putString(KEY_VERSION, next.toString()).commit()180 return next181 }182183 override fun loadSettings(): VaultSettings =184 prefs.getString(KEY_SETTINGS, null)?.let(VaultCodec::decodeSettings) ?: VaultSettings()185186 override fun writeSettings(settings: VaultSettings): Boolean =187 prefs.edit().putString(KEY_SETTINGS, VaultCodec.encodeSettings(settings)).commit()188189 override fun loadVaultHistory(): VaultHistory =190 prefs.getString(KEY_HISTORY, null)?.let(VaultCodec::decodeHistory) ?: VaultHistory()191192 override fun writeVaultHistory(history: VaultHistory): Boolean =193 prefs.edit().putString(KEY_HISTORY, VaultCodec.encodeHistory(history)).commit()194195 override fun clearVaultHistory() {196 prefs.edit().remove(KEY_HISTORY).commit()197 }198199 override fun clearVault() {200 prefs.edit()201 .remove(KEY_SLOT_A).remove(KEY_SLOT_B).remove(KEY_VERSION).remove(KEY_SETTINGS).remove(KEY_HISTORY)202 .commit()203 }204205 companion object {206 private const val PREFS = "peach_vault_store"207 private const val KEY_SLOT_A = "peach_vault_a"208 private const val KEY_SLOT_B = "peach_vault_b"209 private const val KEY_SLOT_DURESS = "peach_vault_duress"210 private const val KEY_VERSION = "peach_last_modified_at"211 private const val KEY_SETTINGS = "peach_settings"212 private const val KEY_HISTORY = "peach_vault_history"213 }214}