fix(marmot): use GCMParameterSpec for decrypt + stop auto-deleting groups on restore failure
Logs from the real device showed that with the previous fixes save and
listGroups now work correctly (878 → 1162 bytes written, file found
after restart), but decrypt was throwing:
InvalidAlgorithmParameterException: Only GCMParameterSpec supported
at AndroidKeyStoreAuthenticatedAESCipherSpi$GCM.initAlgorithmSpecificParameters
AndroidKeyStore's authenticated AES/GCM cipher rejects plain
IvParameterSpec and requires an explicit GCMParameterSpec with the
auth tag length. Switched decrypt() to use GCMParameterSpec(128, iv).
While chasing this I also noticed that MlsGroupManager.restoreAll
was **deleting** the stored group on any exception from load(), on
the assumption that it must be corrupted. This turned a transient
decrypt bug into permanent data loss — the user's group file was
wiped during the first broken restart. Change the catch block to
log and skip instead of delete, so after a fix the next restart
can still recover the data.
https://claude.ai/code/session_014EKS8JBwSpap34aM6FnYLm
This commit is contained in:
+6
-5
@@ -29,7 +29,7 @@ import java.security.KeyStore
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.GCMParameterSpec
|
||||
|
||||
class KeyStoreEncryption {
|
||||
companion object {
|
||||
@@ -42,6 +42,7 @@ class KeyStoreEncryption {
|
||||
private const val PURPOSE = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
|
||||
private const val KEY_ALIAS = "AMETHYST_AES_KEY"
|
||||
private const val GCM_IV_LENGTH = 12
|
||||
private const val GCM_TAG_LENGTH_BITS = 128
|
||||
}
|
||||
|
||||
private val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
@@ -106,12 +107,12 @@ class KeyStoreEncryption {
|
||||
|
||||
fun decrypt(bytes: ByteArray): ByteArray? {
|
||||
try {
|
||||
// Extracts IV and decrypts the data. GCM mode uses a 12-byte IV,
|
||||
// which is what cipher.iv returns in encrypt() — not the AES block
|
||||
// size (16), which is what cipher.blockSize would return.
|
||||
// Extract the 12-byte GCM IV prefix and decrypt the remainder. The
|
||||
// AndroidKeyStore cipher only accepts GCMParameterSpec (not a plain
|
||||
// IvParameterSpec), so we must pass the 128-bit auth tag length.
|
||||
val iv = bytes.copyOfRange(0, GCM_IV_LENGTH)
|
||||
val data = bytes.copyOfRange(GCM_IV_LENGTH, bytes.size)
|
||||
cipher.init(Cipher.DECRYPT_MODE, getKey(), IvParameterSpec(iv))
|
||||
cipher.init(Cipher.DECRYPT_MODE, getKey(), GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv))
|
||||
return cipher.doFinal(data)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "decrypt() failed (input ${bytes.size} bytes): ${e.message}", e)
|
||||
|
||||
+6
-3
@@ -127,13 +127,16 @@ class MlsGroupManager(
|
||||
Log.d(TAG) { "restoreAll(): restored ${retained.size} retained epochs for $nostrGroupId" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Corrupted state — log and remove it so it doesn't block future joins
|
||||
// Could be genuinely corrupted state, or a transient bug
|
||||
// (e.g. wrong cipher param spec). Skip but DO NOT delete —
|
||||
// a future restart after a fix should still be able to
|
||||
// recover the group. If it really is corrupted the user
|
||||
// can explicitly leave/delete the group from the UI.
|
||||
Log.e(
|
||||
TAG,
|
||||
"restoreAll(): Corrupted state for group $nostrGroupId, DELETING: ${e.message}",
|
||||
"restoreAll(): failed to restore group $nostrGroupId, skipping (file preserved): ${e.message}",
|
||||
e,
|
||||
)
|
||||
store.delete(nostrGroupId)
|
||||
}
|
||||
}
|
||||
Log.d(TAG) { "restoreAll(): finished with ${groups.size} active groups in memory" }
|
||||
|
||||
Reference in New Issue
Block a user