From 23b8060222cba9a46689c7a16bedca53e0d7de7d Mon Sep 17 00:00:00 2001 From: Aleksandr Soloshenko Date: Tue, 9 Jan 2024 21:03:55 +0700 Subject: [PATCH] Added: end-to-end encryption --- .github/workflows/docs-publish.yml | 3 + .../5.json | 118 ++++++++++++ app/src/main/java/me/capcom/smsgateway/App.kt | 8 +- .../me/capcom/smsgateway/data/AppDatabase.kt | 5 +- .../smsgateway/data/entities/Message.kt | 2 + .../smsgateway/helpers/SettingsHelper.kt | 13 +- .../modules/encryption/EncryptionService.kt | 72 ++++++++ .../modules/encryption/EncryptionSettings.kt | 16 ++ .../smsgateway/modules/encryption/Module.kt | 9 + .../smsgateway/modules/gateway/GatewayApi.kt | 1 + .../modules/gateway/GatewayModule.kt | 110 ++++++------ .../modules/gateway/GatewaySettings.kt | 22 +++ .../smsgateway/modules/gateway/Module.kt | 7 + .../modules/gateway/PullMessagesWorker.kt | 5 +- .../modules/localserver/WebService.kt | 36 ++-- .../localserver/domain/PostMessageRequest.kt | 1 + .../localserver/domain/PostMessageResponse.kt | 1 + .../modules/messages/MessagesService.kt | 55 +++--- .../smsgateway/modules/messages/Module.kt | 2 +- .../smsgateway/modules/settings/Module.kt | 22 +++ .../capcom/smsgateway/ui/SettingsFragment.kt | 26 ++- .../smsgateway/ui/dialogs/EncryptionDialog.kt | 65 +++++++ app/src/main/res/layout/dialog_encryption.xml | 72 ++++++++ app/src/main/res/layout/fragment_settings.xml | 168 ++++++++++-------- app/src/main/res/values/strings.xml | 6 + .../smsgateway/helpers/PhoneHelperTest.kt | 1 + docs/api/swagger.json | 7 +- 27 files changed, 666 insertions(+), 187 deletions(-) create mode 100644 app/schemas/me.capcom.smsgateway.data.AppDatabase/5.json create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/encryption/EncryptionService.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/encryption/EncryptionSettings.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/encryption/Module.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewaySettings.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/gateway/Module.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/modules/settings/Module.kt create mode 100644 app/src/main/java/me/capcom/smsgateway/ui/dialogs/EncryptionDialog.kt create mode 100644 app/src/main/res/layout/dialog_encryption.xml diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml index 39be7ed..6f17d1a 100644 --- a/.github/workflows/docs-publish.yml +++ b/.github/workflows/docs-publish.yml @@ -13,6 +13,9 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Set App Version + run: | + sed -i "s/\"version\".*/\"version\": \"${GITHUB_REF_NAME#v}\",/" docs/api/swagger.json - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: diff --git a/app/schemas/me.capcom.smsgateway.data.AppDatabase/5.json b/app/schemas/me.capcom.smsgateway.data.AppDatabase/5.json new file mode 100644 index 0000000..35b5c7d --- /dev/null +++ b/app/schemas/me.capcom.smsgateway.data.AppDatabase/5.json @@ -0,0 +1,118 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "4a86263b9bb6a736271cc66c6faca254", + "entities": [ + { + "tableName": "Message", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `text` TEXT NOT NULL, `source` TEXT NOT NULL DEFAULT 'Local', `state` TEXT NOT NULL, `isEncrypted` INTEGER NOT NULL DEFAULT 0, `createdAt` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'Local'" + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isEncrypted", + "columnName": "isEncrypted", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "MessageRecipient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` TEXT NOT NULL, `phoneNumber` TEXT NOT NULL, `state` TEXT NOT NULL, `error` TEXT, PRIMARY KEY(`messageId`, `phoneNumber`), FOREIGN KEY(`messageId`) REFERENCES `Message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "phoneNumber", + "columnName": "phoneNumber", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "messageId", + "phoneNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [ + { + "table": "Message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "messageId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4a86263b9bb6a736271cc66c6faca254')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/App.kt b/app/src/main/java/me/capcom/smsgateway/App.kt index 86ba2ab..caf34a7 100644 --- a/app/src/main/java/me/capcom/smsgateway/App.kt +++ b/app/src/main/java/me/capcom/smsgateway/App.kt @@ -3,10 +3,12 @@ package me.capcom.smsgateway import android.app.Application import androidx.preference.PreferenceManager import me.capcom.smsgateway.data.dbModule +import me.capcom.smsgateway.modules.encryption.encryptionModule import me.capcom.smsgateway.modules.gateway.GatewayModule import me.capcom.smsgateway.modules.localserver.LocalServerModule import me.capcom.smsgateway.modules.messages.messagesModule import me.capcom.smsgateway.modules.settings.PreferencesStorage +import me.capcom.smsgateway.modules.settings.settingsModule import me.capcom.smsgateway.receivers.EventsReceiver import org.koin.android.ext.android.get import org.koin.android.ext.koin.androidContext @@ -21,8 +23,10 @@ class App: Application() { androidLogger() androidContext(this@App) modules( + settingsModule, dbModule, messagesModule, + encryptionModule, ) } @@ -32,11 +36,11 @@ class App: Application() { } val settings by lazy { PreferenceManager.getDefaultSharedPreferences(this) } - + val gatewayModule by lazy { GatewayModule( get(), - PreferencesStorage(settings, "gateway") + get(), ) } val localServerModule by lazy { diff --git a/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt b/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt index ab9bbb0..e13912a 100644 --- a/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt +++ b/app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt @@ -11,11 +11,12 @@ import me.capcom.smsgateway.data.entities.MessageRecipient @Database( entities = [Message::class, MessageRecipient::class], - version = 4, + version = 5, autoMigrations = [ AutoMigration(from = 1, to = 2), AutoMigration(from = 2, to = 3), - AutoMigration(from = 3, to = 4) + AutoMigration(from = 3, to = 4), + AutoMigration(from = 4, to = 5) ] ) @TypeConverters(Converters::class) diff --git a/app/src/main/java/me/capcom/smsgateway/data/entities/Message.kt b/app/src/main/java/me/capcom/smsgateway/data/entities/Message.kt index 5abaff5..a50f432 100644 --- a/app/src/main/java/me/capcom/smsgateway/data/entities/Message.kt +++ b/app/src/main/java/me/capcom/smsgateway/data/entities/Message.kt @@ -10,6 +10,8 @@ data class Message( val text: String, @ColumnInfo(defaultValue = "Local") val source: Source, + @ColumnInfo(defaultValue = "0") + val isEncrypted: Boolean, val state: State = State.Pending, @ColumnInfo(defaultValue = "0") val createdAt: Long = System.currentTimeMillis(), // do we need index here for querying in UI? diff --git a/app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt b/app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt index 6d0ee96..719407d 100644 --- a/app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt +++ b/app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt @@ -30,7 +30,11 @@ class SettingsHelper(private val context: Context) { var serverToken: String get() = settings.getString(PREF_KEY_SERVER_TOKEN, null) - ?: NanoIdUtils.randomNanoId(NanoIdUtils.DEFAULT_NUMBER_GENERATOR, NanoIdUtils.DEFAULT_ALPHABET, 8) + ?: NanoIdUtils.randomNanoId( + NanoIdUtils.DEFAULT_NUMBER_GENERATOR, + NanoIdUtils.DEFAULT_ALPHABET, + 8 + ) .also { serverToken = it } set(value) = settings.edit { putString(PREF_KEY_SERVER_TOKEN, value) } @@ -38,13 +42,6 @@ class SettingsHelper(private val context: Context) { get() = settings.getString(PREF_KEY_FCM_TOKEN, null) set(value) = settings.edit { putString(PREF_KEY_FCM_TOKEN, value) } - data class GatewaySettings( - val id: String, - val token: String, - val login: String, - val password: String, - ) - companion object { private const val PREF_KEY_AUTOSTART = "autostart" diff --git a/app/src/main/java/me/capcom/smsgateway/modules/encryption/EncryptionService.kt b/app/src/main/java/me/capcom/smsgateway/modules/encryption/EncryptionService.kt new file mode 100644 index 0000000..d143856 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/encryption/EncryptionService.kt @@ -0,0 +1,72 @@ +package me.capcom.smsgateway.modules.encryption + +import android.util.Base64 +import javax.crypto.Cipher +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.PBEKeySpec +import javax.crypto.spec.SecretKeySpec + +class EncryptionService( + private val settings: EncryptionSettings, +) { + fun decrypt(encryptedText: String): String { + val chunks = encryptedText.split('$') + if (chunks.size < 5) + throw RuntimeException("Invalid encrypted data format") + + if (chunks[1] != "aes-256-cbc/pbkdf2-sha1") { + throw RuntimeException("Unsupported algorithm") + } + + val params = parseParams(chunks[2]) + if (!params.containsKey("i")) { + throw RuntimeException("Missing iteration count") + } + + val salt = decode(chunks[3]) + val text = chunks[4] + + val passphrase = requireNotNull(settings.passphrase) { "Passphrase is not set" } + val secretKey = generateSecretKeyFromPassphrase( + passphrase.toCharArray(), + salt, + 256, + params.getValue("i").toInt() + ) + + return decryptText(text, secretKey, salt) + } + + private fun decryptText(encryptedText: String, secretKey: SecretKey, iv: ByteArray): String { + val ivSpec = IvParameterSpec(iv) + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec) + val encryptedBytes = decode(encryptedText) + val decryptedBytes = cipher.doFinal(encryptedBytes) + return String(decryptedBytes) + } + + private fun decode(input: String): ByteArray { + return Base64.decode(input, Base64.DEFAULT) + } + + private fun generateSecretKeyFromPassphrase( + passphrase: CharArray, + salt: ByteArray, + keyLength: Int = 256, + iterationCount: Int = 300_000 + ): SecretKey { + val keySpec = PBEKeySpec(passphrase, salt, iterationCount, keyLength) + val keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") + val keyBytes = keyFactory.generateSecret(keySpec).encoded + return SecretKeySpec(keyBytes, "AES") + } + + private fun parseParams(params: String): Map { + return params.split(',') + .map { it.split('=', limit = 2) } + .associate { it[0] to it[1] } + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/encryption/EncryptionSettings.kt b/app/src/main/java/me/capcom/smsgateway/modules/encryption/EncryptionSettings.kt new file mode 100644 index 0000000..a2c21ae --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/encryption/EncryptionSettings.kt @@ -0,0 +1,16 @@ +package me.capcom.smsgateway.modules.encryption + +import me.capcom.smsgateway.modules.settings.KeyValueStorage +import me.capcom.smsgateway.modules.settings.get + +class EncryptionSettings( + private val storage: KeyValueStorage, +) { + var passphrase: String? + get() = storage.get(PASSPHRASE) + set(value) = storage.set(PASSPHRASE, value) + + companion object { + private const val PASSPHRASE = "passphrase" + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/encryption/Module.kt b/app/src/main/java/me/capcom/smsgateway/modules/encryption/Module.kt new file mode 100644 index 0000000..c8f827e --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/encryption/Module.kt @@ -0,0 +1,9 @@ +package me.capcom.smsgateway.modules.encryption + +import org.koin.dsl.module + +val encryptionModule = module { + single { + EncryptionService(get()) + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayApi.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayApi.kt index 11a134c..5faee28 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayApi.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayApi.kt @@ -91,6 +91,7 @@ class GatewayApi() { val phoneNumbers: List, val simNumber: Int? = null, val withDeliveryReport: Boolean? = null, + val isEncrypted: Boolean? = null, ) data class RecipientState( diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayModule.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayModule.kt index 743a09b..3acef32 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayModule.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewayModule.kt @@ -14,20 +14,20 @@ import me.capcom.smsgateway.modules.events.EventBus import me.capcom.smsgateway.modules.gateway.events.DeviceRegisteredEvent import me.capcom.smsgateway.modules.messages.MessageStateChangedEvent import me.capcom.smsgateway.modules.messages.MessagesService -import me.capcom.smsgateway.modules.settings.KeyValueStorage -import me.capcom.smsgateway.modules.settings.get import me.capcom.smsgateway.services.PushService class GatewayModule( private val messagesService: MessagesService, - private val storage: KeyValueStorage, + private val settings: GatewaySettings, ) { private val api = GatewayApi() val events = EventBus() var enabled: Boolean - get() = storage.get(ENABLED) ?: false - set(value) = storage.set(ENABLED, value) + get() = settings.enabled + set(value) { + settings.enabled = value + } fun start(context: Context) { if (!enabled) return @@ -60,8 +60,7 @@ class GatewayModule( private suspend fun sendState( event: MessageStateChangedEvent ) { - val settings = storage.get(REGISTRATION_INFO) - ?: return + val settings = settings.registrationInfo ?: return withContext(Dispatchers.IO) { api.patchMessages( @@ -86,7 +85,7 @@ class GatewayModule( suspend fun registerFcmToken(token: String) { if (!enabled) return - val settings = storage.get(REGISTRATION_INFO) + val settings = settings.registrationInfo settings?.token?.let { withContext(Dispatchers.IO) { api.devicePatch( @@ -104,61 +103,63 @@ class GatewayModule( settings.password, ) ) - } ?: kotlin.run { - val response = withContext(Dispatchers.IO) { - api.deviceRegister( - GatewayApi.DeviceRegisterRequest( - Build.MANUFACTURER + Build.PRODUCT, - token + } + ?: kotlin.run { + val response = withContext(Dispatchers.IO) { + api.deviceRegister( + GatewayApi.DeviceRegisterRequest( + "${Build.MANUFACTURER}/${Build.PRODUCT}", + token + ) ) - ) - } - storage.set(REGISTRATION_INFO, response) + } + this.settings.registrationInfo = response - events.emitEvent( - DeviceRegisteredEvent( - response.login, - response.password, + events.emitEvent( + DeviceRegisteredEvent( + response.login, + response.password, + ) ) - ) - } + } } internal suspend fun getNewMessages() { - val settings = storage.get(REGISTRATION_INFO) ?: return + val settings = settings.registrationInfo ?: return withContext(Dispatchers.IO) { - val messages = api.getMessages(settings.token) - messages.forEach { - try { - messagesService.getMessage(it.id) - ?.also { - sendState( - MessageStateChangedEvent( - it.message.id, - it.message.state, - it.message.source, - it.recipients.map { rcp -> - MessageStateChangedEvent.Recipient( - rcp.phoneNumber, - rcp.state, - rcp.error - ) - } + api.getMessages(settings.token) + .forEach { + try { + messagesService.getMessage(it.id) + ?.also { + sendState( + MessageStateChangedEvent( + it.message.id, + it.message.state, + it.message.source, + it.recipients.map { rcp -> + MessageStateChangedEvent.Recipient( + rcp.phoneNumber, + rcp.state, + rcp.error + ) + } + ) ) + } + ?: messagesService.sendMessage( + it.id, + it.message, + it.phoneNumbers, + Message.Source.Gateway, + it.simNumber?.let { it - 1 }, + it.withDeliveryReport, + it.isEncrypted ?: false, ) - } - ?: messagesService.sendMessage( - it.id, - it.message, - it.phoneNumbers, - Message.Source.Gateway, - it.simNumber?.let { it - 1 }, - it.withDeliveryReport - ) - } catch (th: Throwable) { - th.printStackTrace() + } catch (th: Throwable) { + th.printStackTrace() + } } - } } } @@ -173,8 +174,5 @@ class GatewayModule( companion object { private val job = SupervisorJob() private val scope = CoroutineScope(job) - - private const val REGISTRATION_INFO = "REGISTRATION_INFO" - private const val ENABLED = "ENABLED" } } \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewaySettings.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewaySettings.kt new file mode 100644 index 0000000..0300b5f --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/GatewaySettings.kt @@ -0,0 +1,22 @@ +package me.capcom.smsgateway.modules.gateway + +import me.capcom.smsgateway.modules.settings.KeyValueStorage +import me.capcom.smsgateway.modules.settings.get + +class GatewaySettings( + private val storage: KeyValueStorage, +) { + + var enabled: Boolean + get() = storage.get(ENABLED) ?: false + set(value) = storage.set(ENABLED, value) + + var registrationInfo: GatewayApi.DeviceRegisterResponse? + get() = storage.get(REGISTRATION_INFO) + set(value) = storage.set(REGISTRATION_INFO, value) + + companion object { + private const val REGISTRATION_INFO = "REGISTRATION_INFO" + private const val ENABLED = "ENABLED" + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/Module.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/Module.kt new file mode 100644 index 0000000..b0f1576 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/Module.kt @@ -0,0 +1,7 @@ +package me.capcom.smsgateway.modules.gateway + +import org.koin.dsl.module + +val gatewayModule = module { + single { GatewayModule(get(), get()) } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/gateway/PullMessagesWorker.kt b/app/src/main/java/me/capcom/smsgateway/modules/gateway/PullMessagesWorker.kt index 3fc9601..b6a9051 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/gateway/PullMessagesWorker.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/gateway/PullMessagesWorker.kt @@ -16,8 +16,9 @@ import androidx.work.WorkerParameters import me.capcom.smsgateway.App import java.util.concurrent.TimeUnit -class PullMessagesWorker(appContext: Context, - params: WorkerParameters +class PullMessagesWorker( + appContext: Context, + params: WorkerParameters ) : CoroutineWorker(appContext, params) { override suspend fun doWork(): Result { try { diff --git a/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt b/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt index b2c4806..feb65e1 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/localserver/WebService.kt @@ -114,7 +114,8 @@ class WebService : Service() { request.phoneNumbers, Message.Source.Local, request.simNumber?.let { it - 1 }, - request.withDeliveryReport + request.withDeliveryReport, + request.isEncrypted ?: false, ) } catch (e: IllegalArgumentException) { return@post call.respond( @@ -136,7 +137,8 @@ class WebService : Service() { it.state.toApiState(), it.error ) - } + }, + isEncrypted = message.message.isEncrypted ) ) } @@ -148,20 +150,26 @@ class WebService : Service() { messagesService.getMessage(id) ?: return@get call.respond(HttpStatusCode.NotFound) } catch (e: Throwable) { - return@get call.respond(HttpStatusCode.InternalServerError, mapOf("message" to e.message)) + return@get call.respond( + HttpStatusCode.InternalServerError, + mapOf("message" to e.message) + ) } - call.respond(PostMessageResponse( - message.message.id, - message.message.state.toApiState(), - message.recipients.map { - PostMessageResponse.Recipient( - it.phoneNumber, - it.state.toApiState(), - it.error - ) - } - )) + call.respond( + PostMessageResponse( + message.message.id, + message.message.state.toApiState(), + message.recipients.map { + PostMessageResponse.Recipient( + it.phoneNumber, + it.state.toApiState(), + it.error + ) + }, + message.message.isEncrypted + ) + ) } } } diff --git a/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessageRequest.kt b/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessageRequest.kt index 94f7b9b..2b68de2 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessageRequest.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessageRequest.kt @@ -6,4 +6,5 @@ data class PostMessageRequest( val phoneNumbers: List, val simNumber: Int? = null, val withDeliveryReport: Boolean? = null, + val isEncrypted: Boolean? = null, ) \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessageResponse.kt b/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessageResponse.kt index 10ce7ac..1ba8147 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessageResponse.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/localserver/domain/PostMessageResponse.kt @@ -6,6 +6,7 @@ data class PostMessageResponse( val id: String, val state: MessageState, val recipients: List, + val isEncrypted: Boolean, ) { data class Recipient( diff --git a/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt b/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt index e6fbd44..fc6ed2b 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/messages/MessagesService.kt @@ -18,12 +18,14 @@ import me.capcom.smsgateway.data.entities.Message import me.capcom.smsgateway.data.entities.MessageRecipient import me.capcom.smsgateway.data.entities.MessageWithRecipients import me.capcom.smsgateway.helpers.PhoneHelper +import me.capcom.smsgateway.modules.encryption.EncryptionService import me.capcom.smsgateway.modules.events.EventBus import me.capcom.smsgateway.receivers.EventsReceiver class MessagesService( private val context: Context, private val dao: MessageDao, // todo: use MessagesRepository + private val encryptionService: EncryptionService, ) { val events = EventBus() private val countryCode: String? = @@ -35,29 +37,19 @@ class MessagesService( recipients: List, source: Message.Source, simNumber: Int?, - withDeliveryReport: Boolean? + withDeliveryReport: Boolean?, + isEncrypted: Boolean, ): MessageWithRecipients { val id = id ?: NanoIdUtils.randomNanoId() val message = MessageWithRecipients( - Message(id, text, source), + Message(id, text, source, isEncrypted), recipients.map { - try { - val phoneNumber = PhoneHelper.filterPhoneNumber(it, countryCode ?: "RU") - MessageRecipient( - id, - phoneNumber, - Message.State.Pending - ) - } catch (e: Exception) { - e.printStackTrace() - MessageRecipient( - id, - it, - Message.State.Failed, - "Phone parsing: " + e.message - ) - } + MessageRecipient( + id, + it, + Message.State.Pending + ) }, ) @@ -75,7 +67,8 @@ class MessagesService( message.recipients.filter { it.state == Message.State.Pending } .map { it.phoneNumber }, simNumber, - withDeliveryReport ?: true + withDeliveryReport ?: true, + isEncrypted ) } catch (e: Exception) { e.printStackTrace() @@ -159,9 +152,14 @@ class MessagesService( message: String, recipients: List, simNumber: Int?, - withDeliveryReport: Boolean + withDeliveryReport: Boolean, + isEncrypted: Boolean ) { val smsManager: SmsManager = getSmsManager(simNumber) + val message = when (isEncrypted) { + true -> encryptionService.decrypt(message) + false -> message + } recipients.forEach { val sentIntent = PendingIntent.getBroadcast( @@ -192,17 +190,28 @@ class MessagesService( try { val parts = smsManager.divideMessage(message) - + val phoneNumber = when (isEncrypted) { + true -> encryptionService.decrypt(it) + false -> it + } + val normalizedPhoneNumber = + PhoneHelper.filterPhoneNumber(phoneNumber, countryCode ?: "RU") if (parts.size > 1) { smsManager.sendMultipartTextMessage( - it, + normalizedPhoneNumber, null, parts, ArrayList(parts.map { sentIntent }), deliveredIntent?.let { ArrayList(parts.map { deliveredIntent }) } ) } else { - smsManager.sendTextMessage(it, null, message, sentIntent, deliveredIntent) + smsManager.sendTextMessage( + normalizedPhoneNumber, + null, + message, + sentIntent, + deliveredIntent + ) } updateState(id, it, Message.State.Processed) diff --git a/app/src/main/java/me/capcom/smsgateway/modules/messages/Module.kt b/app/src/main/java/me/capcom/smsgateway/modules/messages/Module.kt index eb8261c..4f0e2f4 100644 --- a/app/src/main/java/me/capcom/smsgateway/modules/messages/Module.kt +++ b/app/src/main/java/me/capcom/smsgateway/modules/messages/Module.kt @@ -8,7 +8,7 @@ import org.koin.dsl.module val messagesModule = module { single { MessagesRepository(get()) } - single { MessagesService(get(), get()) } + single { MessagesService(get(), get(), get()) } viewModel { MessagesListViewModel(get()) } viewModel { MessageDetailsViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/modules/settings/Module.kt b/app/src/main/java/me/capcom/smsgateway/modules/settings/Module.kt new file mode 100644 index 0000000..a8bd6ba --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/modules/settings/Module.kt @@ -0,0 +1,22 @@ +package me.capcom.smsgateway.modules.settings + +import androidx.preference.PreferenceManager +import me.capcom.smsgateway.helpers.SettingsHelper +import me.capcom.smsgateway.modules.encryption.EncryptionSettings +import me.capcom.smsgateway.modules.gateway.GatewaySettings +import org.koin.dsl.module + +val settingsModule = module { + factory { PreferenceManager.getDefaultSharedPreferences(get()) } + factory { SettingsHelper(get()) } + factory { + EncryptionSettings( + PreferencesStorage(get(), "encryption") + ) + } + factory { + GatewaySettings( + PreferencesStorage(get(), "gateway") + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/me/capcom/smsgateway/ui/SettingsFragment.kt b/app/src/main/java/me/capcom/smsgateway/ui/SettingsFragment.kt index 3b7bd64..cde950e 100644 --- a/app/src/main/java/me/capcom/smsgateway/ui/SettingsFragment.kt +++ b/app/src/main/java/me/capcom/smsgateway/ui/SettingsFragment.kt @@ -30,15 +30,19 @@ import me.capcom.smsgateway.App import me.capcom.smsgateway.R import me.capcom.smsgateway.databinding.FragmentSettingsBinding import me.capcom.smsgateway.helpers.SettingsHelper +import me.capcom.smsgateway.modules.encryption.EncryptionSettings import me.capcom.smsgateway.modules.gateway.events.DeviceRegisteredEvent import me.capcom.smsgateway.modules.localserver.events.IPReceivedEvent +import me.capcom.smsgateway.ui.dialogs.EncryptionDialog +import org.koin.android.ext.android.inject class SettingsFragment : Fragment() { private var _binding: FragmentSettingsBinding? = null private val binding get() = _binding!! - private val settingsHelper by lazy { SettingsHelper(requireContext()) } + private val settingsHelper: SettingsHelper by inject() + private val encryptionSettings: EncryptionSettings by inject() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -63,6 +67,10 @@ class SettingsFragment : Fragment() { ) ) + binding.buttonSetUpEncryption.setOnClickListener { + actionSetUpEncryption() + } + binding.switchAutostart.isChecked = settingsHelper.autostart binding.switchAutostart.setOnCheckedChangeListener { _, isChecked -> @@ -129,6 +137,20 @@ class SettingsFragment : Fragment() { stateLiveData.observe(viewLifecycleOwner) { binding.buttonStart.isChecked = it } + + parentFragmentManager.setFragmentResultListener( + ENCRYPTION_DIALOG_REQUEST_KEY, + viewLifecycleOwner + ) { _, bundle -> + encryptionSettings.passphrase = EncryptionDialog.getPassphrase(bundle) + } + } + + private fun actionSetUpEncryption() { + EncryptionDialog.newInstance( + ENCRYPTION_DIALOG_REQUEST_KEY, + encryptionSettings.passphrase ?: "" + ).show(parentFragmentManager, EncryptionDialog::class.java.name) } private fun makeCopyableLink(source: Spanned): Spanned { @@ -243,6 +265,8 @@ class SettingsFragment : Fragment() { } companion object { + private const val ENCRYPTION_DIALOG_REQUEST_KEY = "encryption_dialog_request_key" + @JvmStatic fun newInstance() = SettingsFragment() diff --git a/app/src/main/java/me/capcom/smsgateway/ui/dialogs/EncryptionDialog.kt b/app/src/main/java/me/capcom/smsgateway/ui/dialogs/EncryptionDialog.kt new file mode 100644 index 0000000..7c43ef3 --- /dev/null +++ b/app/src/main/java/me/capcom/smsgateway/ui/dialogs/EncryptionDialog.kt @@ -0,0 +1,65 @@ +package me.capcom.smsgateway.ui.dialogs + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.setFragmentResult +import me.capcom.smsgateway.databinding.DialogEncryptionBinding + + +class EncryptionDialog : DialogFragment() { + + private var _binding: DialogEncryptionBinding? = null + private val binding get() = _binding!! + + private val requestKey by lazy { + requireNotNull(requireArguments().getString(ARG_REQUEST_KEY)) + } + private val passphrase by lazy { + requireArguments().getString(ARG_PASSPHRASE) ?: "" + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + _binding = DialogEncryptionBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.editTextEncryption.setText(passphrase) + + binding.buttonCancel.setOnClickListener { + dismiss() + } + + binding.buttonOk.setOnClickListener { + val passphrase = binding.editTextEncryption.text.toString() + setFragmentResult(requestKey, Bundle().apply { putString(ARG_PASSPHRASE, passphrase) }) + dismiss() + } + +// binding.editTextEncryption.requestFocus() +// WindowCompat.getInsetsController(requireActivity().window, binding.editTextEncryption) +// .show(WindowInsetsCompat.Type.ime()) + } + + companion object { + private const val ARG_REQUEST_KEY = "requestKey" + private const val ARG_PASSPHRASE = "passphrase" + fun newInstance(requestKey: String, passphrase: String) = EncryptionDialog().apply { + arguments = Bundle().apply { + putString(ARG_REQUEST_KEY, requestKey) + putString(ARG_PASSPHRASE, passphrase) + } + } + + fun getPassphrase(bundle: Bundle) = bundle.getString(ARG_PASSPHRASE) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_encryption.xml b/app/src/main/res/layout/dialog_encryption.xml new file mode 100644 index 0000000..be2aa4c --- /dev/null +++ b/app/src/main/res/layout/dialog_encryption.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + +