129 lines
4.8 KiB
Kotlin
129 lines
4.8 KiB
Kotlin
package app.closer.crypto
|
|
|
|
import android.content.Context
|
|
import android.content.SharedPreferences
|
|
import app.closer.data.local.SecurePreferencesFactory
|
|
import com.google.crypto.tink.Aead
|
|
import com.google.crypto.tink.CleartextKeysetHandle
|
|
import com.google.crypto.tink.JsonKeysetReader
|
|
import com.google.crypto.tink.JsonKeysetWriter
|
|
import com.google.crypto.tink.KeysetHandle
|
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
import java.io.ByteArrayOutputStream
|
|
import java.util.concurrent.ConcurrentHashMap
|
|
import javax.inject.Inject
|
|
import javax.inject.Singleton
|
|
|
|
/**
|
|
* Persists Tink keyset handles in EncryptedSharedPreferences (Keystore-backed).
|
|
* Keys are namespaced by coupleId ("keyset_{coupleId}") or invite slot
|
|
* ("keyset_invite_{inviteCode}") for the inviter reconciliation path.
|
|
*/
|
|
@Singleton
|
|
class CoupleKeyStore @Inject constructor(
|
|
@ApplicationContext context: Context
|
|
) {
|
|
private val prefs: SharedPreferences =
|
|
SecurePreferencesFactory.encryptedSharedPreferences(context, PREFS_NAME)
|
|
|
|
private val aeadCache = ConcurrentHashMap<String, Aead>()
|
|
|
|
fun hasKeyset(coupleId: String): Boolean =
|
|
prefs.contains(prefKey(coupleId))
|
|
|
|
fun hasInviteKeyset(inviteCode: String): Boolean =
|
|
prefs.contains(invitePrefKey(inviteCode))
|
|
|
|
fun storeKeyset(coupleId: String, handle: KeysetHandle) {
|
|
val json = serialize(handle)
|
|
prefs.edit().putString(prefKey(coupleId), json).apply()
|
|
aeadCache[coupleId] = handle.getPrimitive(Aead::class.java)
|
|
}
|
|
|
|
fun storeInviteKeyset(inviteCode: String, handle: KeysetHandle) {
|
|
val json = serialize(handle)
|
|
prefs.edit().putString(invitePrefKey(inviteCode), json).apply()
|
|
}
|
|
|
|
fun storeInvitePhrase(inviteCode: String, phrase: String) {
|
|
prefs.edit().putString(invitePhrasePrefKey(inviteCode), phrase).apply()
|
|
}
|
|
|
|
fun storeRecoveryPhrase(coupleId: String, phrase: String) {
|
|
prefs.edit().putString(recoveryPhraseKey(coupleId), phrase).apply()
|
|
}
|
|
|
|
fun loadRecoveryPhrase(coupleId: String): String? =
|
|
prefs.getString(recoveryPhraseKey(coupleId), null)
|
|
|
|
fun loadKeyset(coupleId: String): KeysetHandle? =
|
|
load(prefKey(coupleId))
|
|
|
|
fun loadInviteKeyset(inviteCode: String): KeysetHandle? =
|
|
load(invitePrefKey(inviteCode))
|
|
|
|
/** Moves the invite-slot keyset (and phrase) to the coupleId slot and removes the invite slots. */
|
|
fun reconcileInviteKeyset(inviteCode: String, coupleId: String): Boolean {
|
|
val handle = loadInviteKeyset(inviteCode) ?: return false
|
|
storeKeyset(coupleId, handle)
|
|
prefs.edit().remove(invitePrefKey(inviteCode)).also { editor ->
|
|
val phrase = prefs.getString(invitePhrasePrefKey(inviteCode), null)
|
|
if (phrase != null) {
|
|
editor.putString(recoveryPhraseKey(coupleId), phrase)
|
|
editor.remove(invitePhrasePrefKey(inviteCode))
|
|
}
|
|
}.apply()
|
|
return true
|
|
}
|
|
|
|
fun deleteKeyset(coupleId: String) {
|
|
prefs.edit()
|
|
.remove(prefKey(coupleId))
|
|
.remove(pendingPhraseKey(coupleId))
|
|
.remove(recoveryPhraseKey(coupleId))
|
|
.apply()
|
|
aeadCache.remove(coupleId)
|
|
}
|
|
|
|
fun storePendingRecoveryPhrase(coupleId: String, phrase: String) {
|
|
prefs.edit().putString(pendingPhraseKey(coupleId), phrase).apply()
|
|
}
|
|
|
|
fun pendingRecoveryPhrase(coupleId: String): String? =
|
|
prefs.getString(pendingPhraseKey(coupleId), null)
|
|
|
|
fun clearPendingRecoveryPhrase(coupleId: String) {
|
|
prefs.edit().remove(pendingPhraseKey(coupleId)).apply()
|
|
}
|
|
|
|
fun aeadFor(coupleId: String): Aead? {
|
|
aeadCache[coupleId]?.let { return it }
|
|
val handle = loadKeyset(coupleId) ?: return null
|
|
val aead = handle.getPrimitive(Aead::class.java)
|
|
aeadCache[coupleId] = aead
|
|
return aead
|
|
}
|
|
|
|
private fun prefKey(coupleId: String) = "keyset_$coupleId"
|
|
private fun invitePrefKey(inviteCode: String) = "keyset_invite_$inviteCode"
|
|
private fun invitePhrasePrefKey(inviteCode: String) = "phrase_invite_$inviteCode"
|
|
private fun recoveryPhraseKey(coupleId: String) = "recovery_phrase_$coupleId"
|
|
private fun pendingPhraseKey(coupleId: String) = "pending_recovery_phrase_$coupleId"
|
|
|
|
private fun serialize(handle: KeysetHandle): String {
|
|
val baos = ByteArrayOutputStream()
|
|
CleartextKeysetHandle.write(handle, JsonKeysetWriter.withOutputStream(baos))
|
|
return baos.toString(Charsets.UTF_8.name())
|
|
}
|
|
|
|
private fun load(key: String): KeysetHandle? =
|
|
runCatching {
|
|
val json = prefs.getString(key, null) ?: return null
|
|
CleartextKeysetHandle.read(JsonKeysetReader.withString(json))
|
|
}.getOrNull()
|
|
|
|
private companion object {
|
|
const val PREFS_NAME = "couple_crypto_secure"
|
|
}
|
|
}
|