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 }