Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-megh-l committed Dec 12, 2024
1 parent b3232a7 commit 78bd44a
Show file tree
Hide file tree
Showing 18 changed files with 941 additions and 358 deletions.
6 changes: 3 additions & 3 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -103,12 +103,12 @@ android {
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
buildFeatures {
compose = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class UserJourneyDetailViewModel @Inject constructor(
private fun fetchJourney() = viewModelScope.launch(appDispatcher.IO) {
try {
_state.value = _state.value.copy(isLoading = true)
val journey = journeyService.getLocationJourneyFromId(journeyId)
val journey = journeyService.getLocationJourneyFromId(journeyId, userId)
if (journey == null) {
_state.value = _state.value.copy(
isLoading = false,
Expand Down
1 change: 0 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@ plugins {
id("com.google.firebase.crashlytics") version "3.0.2" apply false
id("org.jetbrains.kotlin.jvm") version "1.9.23"
id("com.google.devtools.ksp") version "1.9.23-1.0.20"
id("com.google.protobuf") version "0.9.4" apply false
}
35 changes: 5 additions & 30 deletions data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ plugins {
id("com.google.dagger.hilt.android")
id("org.jlleitschuh.gradle.ktlint")
id("com.google.devtools.ksp")
id("com.google.protobuf")
}

android {
Expand All @@ -28,19 +27,16 @@ android {
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
isCoreLibraryDesugaringEnabled = true
}
kotlinOptions {
jvmTarget = "1.8"
jvmTarget = "17"
}
ktlint {
debug = true
}
configurations.all {
resolutionStrategy.force("com.google.protobuf:protobuf-javalite:3.10.0")
}
}

dependencies {
Expand Down Expand Up @@ -89,27 +85,6 @@ dependencies {
implementation("com.google.android.libraries.places:places:4.0.0")

// Signal Protocol
implementation("org.whispersystems:signal-protocol-android:2.8.1") {
exclude(group = "com.google.protobuf", module = "protolite-java")
}
implementation("com.google.protobuf:protobuf-javalite:3.10.0") // Align with Signal Protocol version

// AndroidX Security for EncryptedSharedPreferences
implementation("androidx.security:security-crypto:1.1.0-alpha06")

}

protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.10.0"
}
generateProtoTasks {
all().forEach { task ->
task.builtins {
create("java") {
option("lite")
}
}
}
}
implementation("org.signal:libsignal-client:0.64.1")
implementation("org.signal:libsignal-android:0.64.1")
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ data class EncryptedLocationJourney(
val updated_at: Long? = System.currentTimeMillis()
)


@Keep
data class JourneyRoute(val latitude: Double = 0.0, val longitude: Double = 0.0)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class ApiSpace(
val id: String = UUID.randomUUID().toString(),
val admin_id: String = "",
val name: String = "",
val encryptedSenderKeys: Map<String, Map<String, String>> = emptyMap(), // User-specific encrypted keys
val created_at: Long? = System.currentTimeMillis()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package com.canopas.yourspace.data.models.user
import androidx.annotation.Keep
import com.google.firebase.firestore.Exclude
import com.squareup.moshi.JsonClass
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import java.util.UUID

const val LOGIN_TYPE_GOOGLE = 1
Expand All @@ -24,7 +27,12 @@ data class ApiUser(
val state: Int = USER_STATE_UNKNOWN,
val battery_pct: Float? = 0f,
val created_at: Long? = System.currentTimeMillis(),
val updated_at: Long? = System.currentTimeMillis()
val updated_at: Long? = System.currentTimeMillis(),
val public_key: String? = null, // Identity public key (Base64-encoded)
val private_key: String? = null, // Identity private key (Base64-encoded and encrypted)
val pre_keys: List<String>? = null, // List of serialized PreKeys (Base64-encoded)
val signed_pre_key: String? = null, // Serialized Signed PreKey (Base64-encoded)
val registration_id: Int = 0 // Signal Protocol registration ID
) {
@get:Exclude
val fullName: String get() = "$first_name $last_name"
Expand All @@ -39,6 +47,15 @@ data class ApiUser(
val locationPermissionDenied: Boolean get() = state == USER_STATE_LOCATION_PERMISSION_DENIED
}

@Keep
@JsonClass(generateAdapter = false)
data class SignalKeys(
val identityKeyPair: IdentityKeyPair,
val signedPreKey: SignedPreKeyRecord,
val preKeys: List<PreKeyRecord>,
val registrationId: Int
)

@Keep
@JsonClass(generateAdapter = true)
data class ApiUserSession(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.canopas.yourspace.data.security.entity

import org.signal.libsignal.protocol.SignalProtocolAddress

abstract class BaseEncryptedEntity protected constructor(
val registrationId: Int,
val signalProtocolAddress: SignalProtocolAddress
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.canopas.yourspace.data.security.helper

import android.util.Base64

object Helper {
fun encodeToBase64(value: ByteArray?): String {
return Base64.encodeToString(value, Base64.NO_WRAP)
}

fun decodeToByteArray(base64: String?): ByteArray {
return Base64.decode(base64, Base64.NO_WRAP)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
package com.canopas.yourspace.data.security.helper

import android.util.Base64
import com.canopas.yourspace.data.models.user.ApiUser
import com.canopas.yourspace.data.models.user.SignalKeys
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.InvalidKeyException
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.groups.GroupSessionBuilder
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.signal.libsignal.protocol.state.impl.InMemorySignalProtocolStore
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.protocol.util.Medium
import java.util.LinkedList
import java.util.Random
import java.util.UUID
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class SignalKeyHelper @Inject constructor() {

private fun generateIdentityKeyPair(): IdentityKeyPair {
val djbKeyPair = Curve.generateKeyPair()
val djbIdentityKey = IdentityKey(djbKeyPair.publicKey)
val djbPrivateKey = djbKeyPair.privateKey

return IdentityKeyPair(djbIdentityKey, djbPrivateKey)
}

@Throws(InvalidKeyException::class)
fun generateSignedPreKey(
identityKeyPair: IdentityKeyPair,
signedPreKeyId: Int
): SignedPreKeyRecord {
val keyPair = Curve.generateKeyPair()
val signature =
Curve.calculateSignature(identityKeyPair.privateKey, keyPair.publicKey.serialize())
return SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature)
}

private fun generatePreKeys(): List<PreKeyRecord> {
val records: MutableList<PreKeyRecord> = LinkedList()
val preKeyIdOffset = Random().nextInt(Medium.MAX_VALUE - 101)
for (i in 0 until 100) {
val preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE
val keyPair = Curve.generateKeyPair()
val record = PreKeyRecord(preKeyId, keyPair)

records.add(record)
}

return records
}

fun generateSignalKeys(): SignalKeys {
val identityKeyPair = generateIdentityKeyPair()
val signedPreKey = generateSignedPreKey(identityKeyPair, Random().nextInt(Medium.MAX_VALUE - 1))
val preKeys = generatePreKeys()
val registrationId = KeyHelper.generateRegistrationId(false)

return SignalKeys(
identityKeyPair = identityKeyPair,
signedPreKey = signedPreKey,
preKeys = preKeys,
registrationId = registrationId
)
}

fun createDistributionKey(
user: ApiUser,
deviceId: String,
spaceId: String
): Pair<String, String> {
val signalProtocolAddress = SignalProtocolAddress(user.id, deviceId.hashCode())
val identityKeyPair = IdentityKeyPair(
IdentityKey(Curve.decodePoint(Base64.decode(user.public_key, Base64.DEFAULT), 0)),
Curve.decodePrivatePoint(Base64.decode(user.private_key, Base64.DEFAULT))
)
val signalProtocolStore = InMemorySignalProtocolStore(identityKeyPair, user.registration_id)
val signedPreKeyId = SignedPreKeyRecord(Helper.decodeToByteArray(user.signed_pre_key)).id
val preKeys = SignedPreKeyRecord(Helper.decodeToByteArray(user.signed_pre_key))
signalProtocolStore.storeSignedPreKey(signedPreKeyId, preKeys)

user.pre_keys?.let { preKeyRecords ->
val deserializedPreKeys =
preKeyRecords.map { PreKeyRecord(Helper.decodeToByteArray(it)) }
for (record in deserializedPreKeys) {
signalProtocolStore.storePreKey(record.id, record)
}
}
val validSpaceId = try {
UUID.fromString(spaceId) // Validate if it's a proper UUID string
} catch (e: IllegalArgumentException) {
UUID.randomUUID() // Fallback to a new valid UUID if parsing fails
}
signalProtocolStore.storeSenderKey(
signalProtocolAddress,
validSpaceId,
SenderKeyRecord(Helper.decodeToByteArray(user.signed_pre_key))
)

val sessionBuilder = GroupSessionBuilder(signalProtocolStore)
val senderKeyDistributionMessage =
sessionBuilder.create(signalProtocolAddress, validSpaceId)
val senderKeyRecord = signalProtocolStore.loadSenderKey(signalProtocolAddress, validSpaceId)

return Pair(
Helper.encodeToBase64(senderKeyDistributionMessage.serialize()),
Helper.encodeToBase64(senderKeyRecord.serialize())
)
}

private fun encryptAESKeyWithECDH(
aesKey: SecretKey,
publicKey: String,
senderPrivateKey: String
): String {
val ecPublicKey = Curve.decodePoint(Base64.decode(publicKey, Base64.DEFAULT), 0)
val ecPrivateKey = Curve.decodePrivatePoint(Base64.decode(senderPrivateKey, Base64.DEFAULT))
val sharedSecret = Curve.calculateAgreement(ecPublicKey, ecPrivateKey)
val secretKeySpec = SecretKeySpec(sharedSecret, 0, 32, "AES")
val cipher = Cipher.getInstance("AES")
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec)
val encryptedAESKey = cipher.doFinal(aesKey.encoded)

return Base64.encodeToString(encryptedAESKey, Base64.NO_WRAP)
}

private fun decryptAESKeyWithECDH(
encryptedAESKey: String,
privateKey: String,
senderPublicKey: String
): SecretKey {
val ecPublicKey = Curve.decodePoint(Base64.decode(senderPublicKey, Base64.DEFAULT), 0)
val ecPrivateKey = Curve.decodePrivatePoint(Base64.decode(privateKey, Base64.DEFAULT))
val sharedSecret = Curve.calculateAgreement(ecPublicKey, ecPrivateKey)
val secretKeySpec = SecretKeySpec(sharedSecret, 0, 32, "AES")
val cipher = Cipher.getInstance("AES")
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec)
val decryptedAESKeyBytes = cipher.doFinal(Base64.decode(encryptedAESKey, Base64.DEFAULT))

return SecretKeySpec(decryptedAESKeyBytes, "AES")
}

fun encryptSenderKeyForGroup(
senderKey: String,
senderPrivateKey: String,
recipients: List<ApiUser?>
): Map<String, Pair<String, String>> {
val encryptedKeys = mutableMapOf<String, Pair<String, String>>()
val keyGen = KeyGenerator.getInstance("AES")
val aesKey: SecretKey = keyGen.generateKey()
val cipher = Cipher.getInstance("AES")
cipher.init(Cipher.ENCRYPT_MODE, aesKey)
val encryptedSenderKey =
Base64.encodeToString(cipher.doFinal(senderKey.toByteArray()), Base64.NO_WRAP)
recipients.forEach { recipient ->
recipient?.let {
val recipientPublicKey = recipient.public_key!!
val encryptedAESKey =
encryptAESKeyWithECDH(aesKey, recipientPublicKey, senderPrivateKey)
encryptedKeys[recipient.id] = Pair(encryptedSenderKey, encryptedAESKey)
}
}

return encryptedKeys
}

fun decryptSenderKey(
encryptedSenderKey: String,
encryptedAESKey: String,
recipientPrivateKey: String,
senderPublicKey: String
): String {
val aesKey = decryptAESKeyWithECDH(encryptedAESKey, recipientPrivateKey, senderPublicKey)
val cipher = Cipher.getInstance("AES")
cipher.init(Cipher.DECRYPT_MODE, aesKey)
val decryptedSenderKeyBytes =
cipher.doFinal(Base64.decode(encryptedSenderKey, Base64.DEFAULT))

return String(decryptedSenderKeyBytes)
}
}
Loading

0 comments on commit 78bd44a

Please sign in to comment.