E2EEImplementation.kt (6180B) - raw


      1 package me.rhunk.snapenhance.e2ee
      2 
      3 import me.rhunk.snapenhance.RemoteSideContext
      4 import me.rhunk.snapenhance.bridge.e2ee.E2eeInterface
      5 import me.rhunk.snapenhance.bridge.e2ee.EncryptionResult
      6 import me.rhunk.snapenhance.core.util.EvictingMap
      7 import org.bouncycastle.pqc.crypto.crystals.kyber.*
      8 import java.io.File
      9 import java.security.MessageDigest
     10 import java.security.SecureRandom
     11 import javax.crypto.Cipher
     12 import javax.crypto.spec.IvParameterSpec
     13 import javax.crypto.spec.SecretKeySpec
     14 
     15 
     16 class E2EEImplementation (
     17     private val context: RemoteSideContext
     18 ) : E2eeInterface.Stub() {
     19     private val kyberDefaultParameters = KyberParameters.kyber1024
     20     private val secureRandom = SecureRandom()
     21 
     22     private val e2eeFolder by lazy { File(context.androidContext.filesDir, "e2ee").also {
     23         if (!it.exists()) it.mkdirs()
     24     }}
     25     private val pairingFolder by lazy { File(context.androidContext.cacheDir, "e2ee-pairing").also {
     26         if (!it.exists()) it.mkdirs()
     27         else {
     28             it.deleteRecursively()
     29             it.mkdirs()
     30         }
     31     } }
     32 
     33     private val sharedSecretKeyCache = EvictingMap<String, ByteArray?>(100)
     34 
     35     fun storeSharedSecretKey(friendId: String, key: ByteArray) {
     36         File(e2eeFolder, "$friendId.key").writeBytes(key)
     37         sharedSecretKeyCache[friendId] = key
     38     }
     39 
     40     fun getSharedSecretKey(friendId: String): ByteArray? {
     41         return sharedSecretKeyCache.getOrPut(friendId) {
     42             runCatching {
     43                 File(e2eeFolder, "$friendId.key").readBytes()
     44             }.onFailure {
     45                 context.log.warn("Failed to read shared secret key: ${it.message}")
     46             }.getOrNull()
     47         }
     48     }
     49 
     50     fun deleteSharedSecretKey(friendId: String) {
     51         File(e2eeFolder, "$friendId.key").delete()
     52     }
     53 
     54     override fun createKeyExchange(friendId: String): ByteArray? {
     55         val keyPairGenerator = KyberKeyPairGenerator()
     56         keyPairGenerator.init(
     57             KyberKeyGenerationParameters(secureRandom, kyberDefaultParameters)
     58         )
     59         val keyPair = keyPairGenerator.generateKeyPair()
     60         val publicKey = keyPair.public as KyberPublicKeyParameters
     61         val privateKey = keyPair.private as KyberPrivateKeyParameters
     62         runCatching {
     63             File(pairingFolder, "$friendId.private").writeBytes(privateKey.encoded)
     64             File(pairingFolder, "$friendId.public").writeBytes(publicKey.encoded)
     65         }.onFailure {
     66             context.log.error("Failed to write private key to file", it)
     67             return null
     68         }
     69         return publicKey.encoded
     70     }
     71 
     72     override fun acceptPairingRequest(friendId: String, publicKey: ByteArray): ByteArray? {
     73         val kemGen = KyberKEMGenerator(secureRandom)
     74         val encapsulatedSecret = runCatching {
     75             kemGen.generateEncapsulated(
     76                 KyberPublicKeyParameters(
     77                     kyberDefaultParameters,
     78                     publicKey
     79                 )
     80             )
     81         }.onFailure {
     82             context.log.error("Failed to generate encapsulated secret", it)
     83             return null
     84         }.getOrThrow()
     85 
     86         runCatching {
     87             storeSharedSecretKey(friendId, encapsulatedSecret.secret)
     88         }.onFailure {
     89             context.log.error("Failed to store shared secret key", it)
     90             return null
     91         }
     92         return encapsulatedSecret.encapsulation
     93     }
     94 
     95     override fun acceptPairingResponse(friendId: String, encapsulatedSecret: ByteArray): Boolean {
     96         val privateKey = runCatching {
     97             val secretKey = File(pairingFolder, "$friendId.private").readBytes()
     98             object: KyberPrivateKeyParameters(kyberDefaultParameters, null, null, null, null, null) {
     99                 override fun getEncoded() = secretKey
    100             }
    101         }.onFailure {
    102             context.log.error("Failed to read private key from file", it)
    103             return false
    104         }.getOrThrow()
    105 
    106         val kemExtractor = KyberKEMExtractor(privateKey)
    107         val sharedSecret = runCatching {
    108             kemExtractor.extractSecret(encapsulatedSecret)
    109         }.onFailure {
    110             context.log.error("Failed to extract shared secret", it)
    111             return false
    112         }.getOrThrow()
    113 
    114         runCatching {
    115             storeSharedSecretKey(friendId, sharedSecret)
    116         }.onFailure {
    117             context.log.error("Failed to store shared secret key", it)
    118             return false
    119         }
    120 
    121         return true
    122     }
    123 
    124     override fun friendKeyExists(friendId: String): Boolean {
    125         return File(e2eeFolder, "$friendId.key").exists()
    126     }
    127 
    128     override fun getSecretFingerprint(friendId: String): String? {
    129         val sharedSecretKey = getSharedSecretKey(friendId) ?: return null
    130 
    131         return MessageDigest.getInstance("SHA-256")
    132             .digest(sharedSecretKey)
    133             .joinToString("") { "%02x".format(it) }
    134             .chunked(5)
    135             .joinToString(" ")
    136     }
    137 
    138     override fun encryptMessage(friendId: String, message: ByteArray): EncryptionResult? {
    139         val encryptionKey = getSharedSecretKey(friendId) ?: return null
    140 
    141         return runCatching {
    142             val iv = ByteArray(16).apply { secureRandom.nextBytes(this) }
    143             val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
    144             cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(encryptionKey, "AES"), IvParameterSpec(iv))
    145             EncryptionResult().apply {
    146                 this.iv = iv
    147                 this.ciphertext = cipher.doFinal(message)
    148             }
    149         }.onFailure {
    150             context.log.error("Failed to encrypt message for $friendId", it)
    151         }.getOrNull()
    152     }
    153 
    154     override fun decryptMessage(friendId: String, message: ByteArray, iv: ByteArray): ByteArray? {
    155         val encryptionKey = getSharedSecretKey(friendId) ?: return null
    156 
    157         return runCatching {
    158             val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
    159             cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(encryptionKey, "AES"), IvParameterSpec(iv))
    160             cipher.doFinal(message)
    161         }.onFailure {
    162             context.log.warn("Failed to decrypt message for $friendId")
    163             return null
    164         }.getOrNull()
    165     }
    166 }