mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-24 22:45:20 +00:00
fix(security): fail closed on identity key load errors
This commit is contained in:
@@ -415,21 +415,19 @@ open class EncryptionService(private val context: Context) {
|
|||||||
private fun loadOrCreateEd25519KeyPair(): AsymmetricCipherKeyPair {
|
private fun loadOrCreateEd25519KeyPair(): AsymmetricCipherKeyPair {
|
||||||
// Migrate legacy plaintext Ed25519 key to encrypted storage if present
|
// Migrate legacy plaintext Ed25519 key to encrypted storage if present
|
||||||
migrateOldEd25519KeyIfNeeded()
|
migrateOldEd25519KeyIfNeeded()
|
||||||
try {
|
|
||||||
val storedKey = prefs.getString(ED25519_PRIVATE_KEY_PREF, null)
|
|
||||||
|
|
||||||
if (storedKey != null) {
|
val storedKey = prefs.getString(ED25519_PRIVATE_KEY_PREF, null)
|
||||||
// Load existing key
|
if (storedKey != null) {
|
||||||
val privateKeyBytes = Base64.decode(storedKey, Base64.DEFAULT)
|
val privateKeyBytes = Base64.decode(storedKey, Base64.DEFAULT)
|
||||||
val privateKey = Ed25519PrivateKeyParameters(privateKeyBytes, 0)
|
check(privateKeyBytes.size == 32) {
|
||||||
val publicKey = privateKey.generatePublicKey()
|
"Invalid Ed25519 private key size: ${privateKeyBytes.size}"
|
||||||
Log.d(TAG, "✅ Loaded existing Ed25519 signing key pair")
|
|
||||||
return AsymmetricCipherKeyPair(publicKey, privateKey)
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
val privateKey = Ed25519PrivateKeyParameters(privateKeyBytes, 0)
|
||||||
Log.w(TAG, "⚠️ Failed to load existing Ed25519 key, creating new one: ${e.message}")
|
val publicKey = privateKey.generatePublicKey()
|
||||||
|
Log.d(TAG, "✅ Loaded existing Ed25519 signing key pair")
|
||||||
|
return AsymmetricCipherKeyPair(publicKey, privateKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new key pair
|
// Create new key pair
|
||||||
return generateAndSaveEd25519KeyPair()
|
return generateAndSaveEd25519KeyPair()
|
||||||
}
|
}
|
||||||
@@ -445,10 +443,14 @@ open class EncryptionService(private val context: Context) {
|
|||||||
val privateKeyBytes = privateKey.encoded
|
val privateKeyBytes = privateKey.encoded
|
||||||
val encodedKey = Base64.encodeToString(privateKeyBytes, Base64.DEFAULT)
|
val encodedKey = Base64.encodeToString(privateKeyBytes, Base64.DEFAULT)
|
||||||
|
|
||||||
prefs.edit { putString(ED25519_PRIVATE_KEY_PREF, encodedKey) }
|
val saved = prefs.edit()
|
||||||
|
.putString(ED25519_PRIVATE_KEY_PREF, encodedKey)
|
||||||
|
.commit()
|
||||||
|
check(saved) { "Failed to commit Ed25519 private key" }
|
||||||
Log.d(TAG, "✅ Created and stored new Ed25519 signing key pair")
|
Log.d(TAG, "✅ Created and stored new Ed25519 signing key pair")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "❌ Failed to store Ed25519 private key: ${e.message}")
|
Log.e(TAG, "❌ Failed to store Ed25519 private key: ${e.message}")
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
return keyPair
|
return keyPair
|
||||||
@@ -471,7 +473,8 @@ open class EncryptionService(private val context: Context) {
|
|||||||
Log.d(TAG, "🔁 Migrated Ed25519 key to EncryptedSharedPreferences")
|
Log.d(TAG, "🔁 Migrated Ed25519 key to EncryptedSharedPreferences")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "⚠️ Failed to migrate Ed25519 key; generating new identity: ${e.message}")
|
Log.e(TAG, "❌ Failed to migrate Ed25519 key: ${e.message}")
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,30 +60,11 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
* Returns (privateKey, publicKey) or null if none exists
|
* Returns (privateKey, publicKey) or null if none exists
|
||||||
*/
|
*/
|
||||||
fun loadStaticKey(): Pair<ByteArray, ByteArray>? {
|
fun loadStaticKey(): Pair<ByteArray, ByteArray>? {
|
||||||
return try {
|
return loadKeyPair(
|
||||||
val privateKeyString = prefs.getString(KEY_STATIC_PRIVATE_KEY, null)
|
privateKeyName = KEY_STATIC_PRIVATE_KEY,
|
||||||
val publicKeyString = prefs.getString(KEY_STATIC_PUBLIC_KEY, null)
|
publicKeyName = KEY_STATIC_PUBLIC_KEY,
|
||||||
|
keyDescription = "static identity key"
|
||||||
if (privateKeyString != null && publicKeyString != null) {
|
)
|
||||||
val privateKey = android.util.Base64.decode(privateKeyString, android.util.Base64.DEFAULT)
|
|
||||||
val publicKey = android.util.Base64.decode(publicKeyString, android.util.Base64.DEFAULT)
|
|
||||||
|
|
||||||
// Validate key sizes
|
|
||||||
if (privateKey.size == 32 && publicKey.size == 32) {
|
|
||||||
Log.d(TAG, "Loaded static identity key from secure storage")
|
|
||||||
Pair(privateKey, publicKey)
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Invalid key sizes in storage, returning null")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(TAG, "No static identity key found in storage")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to load static key: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,10 +80,11 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
val privateKeyString = android.util.Base64.encodeToString(privateKey, android.util.Base64.DEFAULT)
|
val privateKeyString = android.util.Base64.encodeToString(privateKey, android.util.Base64.DEFAULT)
|
||||||
val publicKeyString = android.util.Base64.encodeToString(publicKey, android.util.Base64.DEFAULT)
|
val publicKeyString = android.util.Base64.encodeToString(publicKey, android.util.Base64.DEFAULT)
|
||||||
|
|
||||||
prefs.edit()
|
val saved = prefs.edit()
|
||||||
.putString(KEY_STATIC_PRIVATE_KEY, privateKeyString)
|
.putString(KEY_STATIC_PRIVATE_KEY, privateKeyString)
|
||||||
.putString(KEY_STATIC_PUBLIC_KEY, publicKeyString)
|
.putString(KEY_STATIC_PUBLIC_KEY, publicKeyString)
|
||||||
.apply()
|
.commit()
|
||||||
|
check(saved) { "Failed to commit static identity key" }
|
||||||
|
|
||||||
Log.d(TAG, "Saved static identity key to secure storage")
|
Log.d(TAG, "Saved static identity key to secure storage")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -118,30 +100,39 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
* Returns (privateKey, publicKey) or null if none exists
|
* Returns (privateKey, publicKey) or null if none exists
|
||||||
*/
|
*/
|
||||||
fun loadSigningKey(): Pair<ByteArray, ByteArray>? {
|
fun loadSigningKey(): Pair<ByteArray, ByteArray>? {
|
||||||
return try {
|
return loadKeyPair(
|
||||||
val privateKeyString = prefs.getString(KEY_SIGNING_PRIVATE_KEY, null)
|
privateKeyName = KEY_SIGNING_PRIVATE_KEY,
|
||||||
val publicKeyString = prefs.getString(KEY_SIGNING_PUBLIC_KEY, null)
|
publicKeyName = KEY_SIGNING_PUBLIC_KEY,
|
||||||
|
keyDescription = "Ed25519 signing key"
|
||||||
if (privateKeyString != null && publicKeyString != null) {
|
)
|
||||||
val privateKey = android.util.Base64.decode(privateKeyString, android.util.Base64.DEFAULT)
|
}
|
||||||
val publicKey = android.util.Base64.decode(publicKeyString, android.util.Base64.DEFAULT)
|
|
||||||
|
private fun loadKeyPair(
|
||||||
// Validate key sizes
|
privateKeyName: String,
|
||||||
if (privateKey.size == 32 && publicKey.size == 32) {
|
publicKeyName: String,
|
||||||
Log.d(TAG, "Loaded Ed25519 signing key from secure storage")
|
keyDescription: String
|
||||||
Pair(privateKey, publicKey)
|
): Pair<ByteArray, ByteArray>? {
|
||||||
} else {
|
val privateKeyString = prefs.getString(privateKeyName, null)
|
||||||
Log.w(TAG, "Invalid signing key sizes in storage, returning null")
|
val publicKeyString = prefs.getString(publicKeyName, null)
|
||||||
null
|
|
||||||
}
|
if (privateKeyString == null && publicKeyString == null) {
|
||||||
} else {
|
Log.d(TAG, "No $keyDescription found in storage")
|
||||||
Log.d(TAG, "No Ed25519 signing key found in storage")
|
return null
|
||||||
null
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to load signing key: ${e.message}")
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val privateEncoded = privateKeyString
|
||||||
|
?: error("Incomplete $keyDescription in secure storage")
|
||||||
|
val publicEncoded = publicKeyString
|
||||||
|
?: error("Incomplete $keyDescription in secure storage")
|
||||||
|
|
||||||
|
val privateKey = Base64.decode(privateEncoded, Base64.DEFAULT)
|
||||||
|
val publicKey = Base64.decode(publicEncoded, Base64.DEFAULT)
|
||||||
|
check(privateKey.size == 32 && publicKey.size == 32) {
|
||||||
|
"Invalid $keyDescription sizes: private=${privateKey.size}, public=${publicKey.size}"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Loaded $keyDescription from secure storage")
|
||||||
|
return Pair(privateKey, publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -157,10 +148,11 @@ class SecureIdentityStateManager(private val context: Context) {
|
|||||||
val privateKeyString = android.util.Base64.encodeToString(privateKey, android.util.Base64.DEFAULT)
|
val privateKeyString = android.util.Base64.encodeToString(privateKey, android.util.Base64.DEFAULT)
|
||||||
val publicKeyString = android.util.Base64.encodeToString(publicKey, android.util.Base64.DEFAULT)
|
val publicKeyString = android.util.Base64.encodeToString(publicKey, android.util.Base64.DEFAULT)
|
||||||
|
|
||||||
prefs.edit()
|
val saved = prefs.edit()
|
||||||
.putString(KEY_SIGNING_PRIVATE_KEY, privateKeyString)
|
.putString(KEY_SIGNING_PRIVATE_KEY, privateKeyString)
|
||||||
.putString(KEY_SIGNING_PUBLIC_KEY, publicKeyString)
|
.putString(KEY_SIGNING_PUBLIC_KEY, publicKeyString)
|
||||||
.apply()
|
.commit()
|
||||||
|
check(saved) { "Failed to commit Ed25519 signing key" }
|
||||||
|
|
||||||
Log.d(TAG, "Saved Ed25519 signing key to secure storage")
|
Log.d(TAG, "Saved Ed25519 signing key to secure storage")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.bitchat.android.identity
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Base64
|
||||||
|
import androidx.test.core.app.ApplicationProvider
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Assert.assertNull
|
||||||
|
import org.junit.Assert.assertThrows
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.RobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(RobolectricTestRunner::class)
|
||||||
|
class SecureIdentityStateManagerTest {
|
||||||
|
private lateinit var manager: SecureIdentityStateManager
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||||
|
manager = SecureIdentityStateManager(context)
|
||||||
|
manager.clearIdentityData()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
manager.clearIdentityData()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadStaticKey_returnsNullWhenBothKeysAreAbsent() {
|
||||||
|
assertNull(manager.loadStaticKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadStaticKey_throwsWhenOnlyOneKeyExists() {
|
||||||
|
manager.storeSecureValue("static_private_key", encoded(ByteArray(32) { 1 }))
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException::class.java) {
|
||||||
|
manager.loadStaticKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun loadSigningKey_throwsWhenStoredKeyHasInvalidSize() {
|
||||||
|
manager.storeSecureValue("signing_private_key", encoded(ByteArray(31) { 1 }))
|
||||||
|
manager.storeSecureValue("signing_public_key", encoded(ByteArray(32) { 2 }))
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException::class.java) {
|
||||||
|
manager.loadSigningKey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun encoded(bytes: ByteArray): String {
|
||||||
|
return Base64.encodeToString(bytes, Base64.DEFAULT)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user