Add file-level locking to prevent race conditions in key persistence
Per-key ReentrantLock prevents concurrent writes to same key file
This commit is contained in:
+64
-40
@@ -7,9 +7,12 @@ import java.io.DataOutputStream
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.cert.Certificate
|
import java.security.cert.Certificate
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.locks.ReentrantLock
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
import org.matrix.TEESimulator.config.ConfigurationManager.CONFIG_PATH
|
||||||
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
import org.matrix.TEESimulator.interception.keystore.KeyIdentifier
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
@@ -35,6 +38,13 @@ object GeneratedKeyPersistence {
|
|||||||
private const val FORMAT_VERSION = 1
|
private const val FORMAT_VERSION = 1
|
||||||
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys")
|
||||||
|
|
||||||
|
// Per-filename locks to prevent concurrent writes to the same key file
|
||||||
|
private val fileLocks = ConcurrentHashMap<String, ReentrantLock>()
|
||||||
|
|
||||||
|
private fun getLockForKey(filename: String): ReentrantLock {
|
||||||
|
return fileLocks.computeIfAbsent(filename) { ReentrantLock() }
|
||||||
|
}
|
||||||
|
|
||||||
fun save(
|
fun save(
|
||||||
keyId: KeyIdentifier,
|
keyId: KeyIdentifier,
|
||||||
keyPair: KeyPair,
|
keyPair: KeyPair,
|
||||||
@@ -48,55 +58,69 @@ object GeneratedKeyPersistence {
|
|||||||
digests: List<Int>,
|
digests: List<Int>,
|
||||||
isAttestationKey: Boolean,
|
isAttestationKey: Boolean,
|
||||||
) {
|
) {
|
||||||
runCatching {
|
val filename = keyFileName(keyId.uid, keyId.alias)
|
||||||
PERSISTENCE_DIR.mkdirs()
|
val lock = getLockForKey(filename)
|
||||||
val filename = keyFileName(keyId.uid, keyId.alias)
|
SystemLogger.debug("[Persistence] Acquiring lock for $filename")
|
||||||
val finalFile = File(PERSISTENCE_DIR, filename)
|
lock.lock()
|
||||||
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
|
try {
|
||||||
|
SystemLogger.debug("[Persistence] Lock acquired for $filename")
|
||||||
|
runCatching {
|
||||||
|
PERSISTENCE_DIR.mkdirs()
|
||||||
|
val finalFile = File(PERSISTENCE_DIR, filename)
|
||||||
|
val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out ->
|
DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out ->
|
||||||
out.writeInt(FORMAT_VERSION)
|
out.writeInt(FORMAT_VERSION)
|
||||||
out.writeInt(securityLevel)
|
out.writeInt(securityLevel)
|
||||||
out.writeInt(keyId.uid)
|
out.writeInt(keyId.uid)
|
||||||
out.writeUTF(keyId.alias)
|
out.writeUTF(keyId.alias)
|
||||||
out.writeLong(nspace)
|
out.writeLong(nspace)
|
||||||
out.writeBoolean(isAttestationKey)
|
out.writeBoolean(isAttestationKey)
|
||||||
out.writeInt(algorithm)
|
out.writeInt(algorithm)
|
||||||
out.writeInt(keySize)
|
out.writeInt(keySize)
|
||||||
out.writeInt(ecCurve)
|
out.writeInt(ecCurve)
|
||||||
|
|
||||||
out.writeInt(purposes.size)
|
out.writeInt(purposes.size)
|
||||||
purposes.forEach { out.writeInt(it) }
|
purposes.forEach { out.writeInt(it) }
|
||||||
|
|
||||||
out.writeInt(digests.size)
|
out.writeInt(digests.size)
|
||||||
digests.forEach { out.writeInt(it) }
|
digests.forEach { out.writeInt(it) }
|
||||||
|
|
||||||
val pkBytes = keyPair.private.encoded
|
val pkBytes = keyPair.private.encoded
|
||||||
out.writeInt(pkBytes.size)
|
out.writeInt(pkBytes.size)
|
||||||
out.write(pkBytes)
|
out.write(pkBytes)
|
||||||
|
|
||||||
out.writeInt(certChain.size)
|
out.writeInt(certChain.size)
|
||||||
certChain.forEach { cert ->
|
certChain.forEach { cert ->
|
||||||
val encoded = cert.encoded
|
val encoded = cert.encoded
|
||||||
out.writeInt(encoded.size)
|
out.writeInt(encoded.size)
|
||||||
out.write(encoded)
|
out.write(encoded)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
tmpFile.delete()
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
tmpFile.delete()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll
|
// Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll
|
||||||
if (!tmpFile.renameTo(finalFile)) {
|
if (!tmpFile.renameTo(finalFile)) {
|
||||||
tmpFile.delete()
|
tmpFile.delete()
|
||||||
throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile")
|
throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile")
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemLogger.debug("Persisted key: $keyId")
|
// Verify write succeeded - catches disk-full or filesystem errors
|
||||||
}.onFailure { e ->
|
if (!finalFile.exists() || finalFile.length() < 20) {
|
||||||
SystemLogger.error("Failed to persist key $keyId", e)
|
throw IOException("File write verification failed - possible disk full")
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemLogger.debug("Persisted key: $keyId")
|
||||||
|
}.onFailure { e ->
|
||||||
|
SystemLogger.error("Failed to persist key $keyId", e)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
lock.unlock()
|
||||||
|
SystemLogger.debug("[Persistence] Lock released for $filename")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user