mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-07-25 08:45:20 +00:00
Fix/590-Private-key-stored-as-Plain-text (#603)
* Private Key Stored in Plaintext(SharedPreference) in EncryptionService migrated to EncryptedSharedPreferences. * Fix: Safe migration to EncryptedSharedPreferences and restore testability - Use distinct filename for encrypted prefs to avoid collision with legacy plaintext file - Move Keystore setup to initialize() to support Robolectric test mocks - Implement safe read-old/write-new migration logic --------- Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
package com.bitchat.android.crypto
|
package com.bitchat.android.crypto
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import android.util.Base64
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.security.crypto.EncryptedSharedPreferences
|
||||||
|
import androidx.security.crypto.MasterKey
|
||||||
import com.bitchat.android.noise.NoiseEncryptionService
|
import com.bitchat.android.noise.NoiseEncryptionService
|
||||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair
|
||||||
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
|
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
|
||||||
@@ -11,6 +15,7 @@ import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters
|
|||||||
import org.bouncycastle.crypto.signers.Ed25519Signer
|
import org.bouncycastle.crypto.signers.Ed25519Signer
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import androidx.core.content.edit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encryption service that now uses NoiseEncryptionService internally
|
* Encryption service that now uses NoiseEncryptionService internally
|
||||||
@@ -24,6 +29,8 @@ open class EncryptionService(private val context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "EncryptionService"
|
private const val TAG = "EncryptionService"
|
||||||
private const val ED25519_PRIVATE_KEY_PREF = "ed25519_signing_private_key"
|
private const val ED25519_PRIVATE_KEY_PREF = "ed25519_signing_private_key"
|
||||||
|
private const val OLD_PREFS_NAME = "bitchat_crypto"
|
||||||
|
private const val SECURE_PREFS_NAME = "bitchat_crypto_secure"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core Noise encryption service
|
// Core Noise encryption service
|
||||||
@@ -40,15 +47,32 @@ open class EncryptionService(private val context: Context) {
|
|||||||
var onSessionEstablished: ((String) -> Unit)? = null // peerID
|
var onSessionEstablished: ((String) -> Unit)? = null // peerID
|
||||||
var onSessionLost: ((String) -> Unit)? = null // peerID
|
var onSessionLost: ((String) -> Unit)? = null // peerID
|
||||||
var onHandshakeRequired: ((String) -> Unit)? = null // peerID
|
var onHandshakeRequired: ((String) -> Unit)? = null // peerID
|
||||||
|
private lateinit var prefs: SharedPreferences
|
||||||
|
|
||||||
init {
|
init {
|
||||||
initialize()
|
initialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setUpEncryptedPrefs() {
|
||||||
|
val masterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
|
||||||
|
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
// Create encrypted shared preferences
|
||||||
|
prefs = EncryptedSharedPreferences.create(
|
||||||
|
context,
|
||||||
|
SECURE_PREFS_NAME,
|
||||||
|
masterKey,
|
||||||
|
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
|
||||||
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialization logic moved to method to allow overriding in tests
|
* Initialization logic moved to method to allow overriding in tests
|
||||||
*/
|
*/
|
||||||
protected open fun initialize() {
|
protected open fun initialize() {
|
||||||
|
setUpEncryptedPrefs()
|
||||||
// Initialize or load Ed25519 signing keys
|
// Initialize or load Ed25519 signing keys
|
||||||
val keyPair = loadOrCreateEd25519KeyPair()
|
val keyPair = loadOrCreateEd25519KeyPair()
|
||||||
ed25519PrivateKey = keyPair.private as Ed25519PrivateKeyParameters
|
ed25519PrivateKey = keyPair.private as Ed25519PrivateKeyParameters
|
||||||
@@ -142,8 +166,7 @@ open class EncryptionService(private val context: Context) {
|
|||||||
|
|
||||||
// Clear Ed25519 signing key from preferences
|
// Clear Ed25519 signing key from preferences
|
||||||
try {
|
try {
|
||||||
val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE)
|
prefs.edit { remove(ED25519_PRIVATE_KEY_PREF) }
|
||||||
prefs.edit().remove(ED25519_PRIVATE_KEY_PREF).apply()
|
|
||||||
Log.d(TAG, "🗑️ Cleared Ed25519 signing keys from preferences")
|
Log.d(TAG, "🗑️ Cleared Ed25519 signing keys from preferences")
|
||||||
|
|
||||||
// Generate new keys immediately
|
// Generate new keys immediately
|
||||||
@@ -390,13 +413,14 @@ open class EncryptionService(private val context: Context) {
|
|||||||
* Load existing Ed25519 key pair from preferences or create a new one
|
* Load existing Ed25519 key pair from preferences or create a new one
|
||||||
*/
|
*/
|
||||||
private fun loadOrCreateEd25519KeyPair(): AsymmetricCipherKeyPair {
|
private fun loadOrCreateEd25519KeyPair(): AsymmetricCipherKeyPair {
|
||||||
|
// Migrate legacy plaintext Ed25519 key to encrypted storage if present
|
||||||
|
migrateOldEd25519KeyIfNeeded()
|
||||||
try {
|
try {
|
||||||
val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE)
|
|
||||||
val storedKey = prefs.getString(ED25519_PRIVATE_KEY_PREF, null)
|
val storedKey = prefs.getString(ED25519_PRIVATE_KEY_PREF, null)
|
||||||
|
|
||||||
if (storedKey != null) {
|
if (storedKey != null) {
|
||||||
// Load existing key
|
// Load existing key
|
||||||
val privateKeyBytes = android.util.Base64.decode(storedKey, android.util.Base64.DEFAULT)
|
val privateKeyBytes = Base64.decode(storedKey, Base64.DEFAULT)
|
||||||
val privateKey = Ed25519PrivateKeyParameters(privateKeyBytes, 0)
|
val privateKey = Ed25519PrivateKeyParameters(privateKeyBytes, 0)
|
||||||
val publicKey = privateKey.generatePublicKey()
|
val publicKey = privateKey.generatePublicKey()
|
||||||
Log.d(TAG, "✅ Loaded existing Ed25519 signing key pair")
|
Log.d(TAG, "✅ Loaded existing Ed25519 signing key pair")
|
||||||
@@ -407,6 +431,10 @@ open class EncryptionService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new key pair
|
// Create new key pair
|
||||||
|
return generateAndSaveEd25519KeyPair()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateAndSaveEd25519KeyPair(): AsymmetricCipherKeyPair {
|
||||||
val keyGen = Ed25519KeyPairGenerator()
|
val keyGen = Ed25519KeyPairGenerator()
|
||||||
keyGen.init(Ed25519KeyGenerationParameters(SecureRandom()))
|
keyGen.init(Ed25519KeyGenerationParameters(SecureRandom()))
|
||||||
val keyPair = keyGen.generateKeyPair()
|
val keyPair = keyGen.generateKeyPair()
|
||||||
@@ -415,10 +443,9 @@ open class EncryptionService(private val context: Context) {
|
|||||||
try {
|
try {
|
||||||
val privateKey = keyPair.private as Ed25519PrivateKeyParameters
|
val privateKey = keyPair.private as Ed25519PrivateKeyParameters
|
||||||
val privateKeyBytes = privateKey.encoded
|
val privateKeyBytes = privateKey.encoded
|
||||||
val encodedKey = android.util.Base64.encodeToString(privateKeyBytes, android.util.Base64.DEFAULT)
|
val encodedKey = Base64.encodeToString(privateKeyBytes, Base64.DEFAULT)
|
||||||
|
|
||||||
val prefs = context.getSharedPreferences("bitchat_crypto", Context.MODE_PRIVATE)
|
prefs.edit { putString(ED25519_PRIVATE_KEY_PREF, encodedKey) }
|
||||||
prefs.edit().putString(ED25519_PRIVATE_KEY_PREF, encodedKey).apply()
|
|
||||||
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}")
|
||||||
@@ -426,4 +453,25 @@ open class EncryptionService(private val context: Context) {
|
|||||||
|
|
||||||
return keyPair
|
return keyPair
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun migrateOldEd25519KeyIfNeeded() {
|
||||||
|
try {
|
||||||
|
// old existing plain text preference
|
||||||
|
val oldPrefs = context.getSharedPreferences(OLD_PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
val oldKey = oldPrefs.getString(ED25519_PRIVATE_KEY_PREF, null)
|
||||||
|
|
||||||
|
if (oldKey != null && !prefs.contains(ED25519_PRIVATE_KEY_PREF)) {
|
||||||
|
prefs.edit {
|
||||||
|
putString(ED25519_PRIVATE_KEY_PREF, oldKey)
|
||||||
|
}
|
||||||
|
oldPrefs.edit {
|
||||||
|
remove(ED25519_PRIVATE_KEY_PREF)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "🔁 Migrated Ed25519 key to EncryptedSharedPreferences")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "⚠️ Failed to migrate Ed25519 key; generating new identity: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user