Skip to content

Commit

Permalink
Merge pull request #31 from capcom6/feature/encryption
Browse files Browse the repository at this point in the history
End-to-end encryption support
  • Loading branch information
capcom6 authored Jan 26, 2024
2 parents 0a309a0 + 23b8060 commit 1d992f0
Show file tree
Hide file tree
Showing 27 changed files with 666 additions and 187 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/docs-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
118 changes: 118 additions & 0 deletions app/schemas/me.capcom.smsgateway.data.AppDatabase/5.json
Original file line number Diff line number Diff line change
@@ -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')"
]
}
}
8 changes: 6 additions & 2 deletions app/src/main/java/me/capcom/smsgateway/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -21,8 +23,10 @@ class App: Application() {
androidLogger()
androidContext(this@App)
modules(
settingsModule,
dbModule,
messagesModule,
encryptionModule,
)
}

Expand All @@ -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 {
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/me/capcom/smsgateway/data/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
13 changes: 5 additions & 8 deletions app/src/main/java/me/capcom/smsgateway/helpers/SettingsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,18 @@ 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) }

var fcmToken: String?
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"

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> {
return params.split(',')
.map { it.split('=', limit = 2) }
.associate { it[0] to it[1] }
}
}
Original file line number Diff line number Diff line change
@@ -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<String>(PASSPHRASE)
set(value) = storage.set(PASSPHRASE, value)

companion object {
private const val PASSPHRASE = "passphrase"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package me.capcom.smsgateway.modules.encryption

import org.koin.dsl.module

val encryptionModule = module {
single {
EncryptionService(get())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ class GatewayApi() {
val phoneNumbers: List<String>,
val simNumber: Int? = null,
val withDeliveryReport: Boolean? = null,
val isEncrypted: Boolean? = null,
)

data class RecipientState(
Expand Down
Loading

0 comments on commit 1d992f0

Please sign in to comment.