diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7aaae700..8f8fa696 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 diff --git a/app/src/main/java/com/canopas/yourspace/YourSpaceApplication.kt b/app/src/main/java/com/canopas/yourspace/YourSpaceApplication.kt index 280911f1..260071a3 100644 --- a/app/src/main/java/com/canopas/yourspace/YourSpaceApplication.kt +++ b/app/src/main/java/com/canopas/yourspace/YourSpaceApplication.kt @@ -53,7 +53,7 @@ class YourSpaceApplication : super.onCreate() Timber.plant(Timber.DebugTree()) - FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = !BuildConfig.DEBUG + FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = false ProcessLifecycleOwner.get().lifecycle.addObserver(this) authService.addListener(this) setNotificationChannel() diff --git a/app/src/main/java/com/canopas/yourspace/ui/flow/journey/detail/UserJourneyDetailViewModel.kt b/app/src/main/java/com/canopas/yourspace/ui/flow/journey/detail/UserJourneyDetailViewModel.kt index 3e6c7d3d..1177147e 100644 --- a/app/src/main/java/com/canopas/yourspace/ui/flow/journey/detail/UserJourneyDetailViewModel.kt +++ b/app/src/main/java/com/canopas/yourspace/ui/flow/journey/detail/UserJourneyDetailViewModel.kt @@ -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(userId, journeyId) + val journey = journeyService.getLocationJourneyFromId(journeyId, userId) if (journey == null) { _state.value = _state.value.copy( isLoading = false, diff --git a/data/build.gradle.kts b/data/build.gradle.kts index e0d79d8e..8dde311c 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -27,12 +27,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" } ktlint { debug = true @@ -83,4 +83,8 @@ dependencies { // Place implementation("com.google.android.libraries.places:places:4.0.0") + + // Signal Protocol + implementation("org.signal:libsignal-client:0.64.1") + implementation("org.signal:libsignal-android:0.64.1") } diff --git a/data/proguard-rules.pro b/data/proguard-rules.pro index 481bb434..4402fd6f 100644 --- a/data/proguard-rules.pro +++ b/data/proguard-rules.pro @@ -18,4 +18,6 @@ # If you keep the line number information, uncomment this to # hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file +#-renamesourcefileattribute SourceFile +-keep class com.google.protobuf.** { *; } +-keep class org.whispersystems.** { *; } \ No newline at end of file diff --git a/data/src/main/java/com/canopas/yourspace/data/models/location/ApiLocation.kt b/data/src/main/java/com/canopas/yourspace/data/models/location/ApiLocation.kt index 274383f7..04d34497 100644 --- a/data/src/main/java/com/canopas/yourspace/data/models/location/ApiLocation.kt +++ b/data/src/main/java/com/canopas/yourspace/data/models/location/ApiLocation.kt @@ -14,6 +14,16 @@ data class ApiLocation( val created_at: Long? = System.currentTimeMillis() ) +@Keep +@JsonClass(generateAdapter = true) +data class EncryptedApiLocation( + val id: String = UUID.randomUUID().toString(), + val user_id: String = "", + val encrypted_latitude: String = "", // Base64 encoded encrypted latitude + val encrypted_longitude: String = "", // Base64 encoded encrypted longitude + val created_at: Long? = System.currentTimeMillis() +) + fun ApiLocation.toLocation() = android.location.Location("").apply { latitude = this@toLocation.latitude longitude = this@toLocation.longitude diff --git a/data/src/main/java/com/canopas/yourspace/data/models/location/LocationJourney.kt b/data/src/main/java/com/canopas/yourspace/data/models/location/LocationJourney.kt index d01a572c..d2b9e617 100644 --- a/data/src/main/java/com/canopas/yourspace/data/models/location/LocationJourney.kt +++ b/data/src/main/java/com/canopas/yourspace/data/models/location/LocationJourney.kt @@ -22,9 +22,32 @@ data class LocationJourney( val update_at: Long? = System.currentTimeMillis() ) +@Keep +@JsonClass(generateAdapter = true) +data class EncryptedLocationJourney( + val id: String = UUID.randomUUID().toString(), + val user_id: String = "", + val encrypted_from_latitude: String = "", // Base64 encoded + val encrypted_from_longitude: String = "", + val encrypted_to_latitude: String? = "", + val encrypted_to_longitude: String? = "", + val route_distance: Double? = null, + val route_duration: Long? = null, + val encrypted_routes: List = emptyList(), + val created_at: Long? = System.currentTimeMillis(), + val updated_at: Long? = System.currentTimeMillis() +) + @Keep data class JourneyRoute(val latitude: Double = 0.0, val longitude: Double = 0.0) +@Keep +@JsonClass(generateAdapter = true) +data class EncryptedJourneyRoute( + val encrypted_latitude: String = "", + val encrypted_longitude: String = "" +) + fun Location.toRoute(): JourneyRoute { return JourneyRoute(latitude, longitude) } diff --git a/data/src/main/java/com/canopas/yourspace/data/models/space/ApiSpace.kt b/data/src/main/java/com/canopas/yourspace/data/models/space/ApiSpace.kt index 0d83f35b..692aabe2 100644 --- a/data/src/main/java/com/canopas/yourspace/data/models/space/ApiSpace.kt +++ b/data/src/main/java/com/canopas/yourspace/data/models/space/ApiSpace.kt @@ -10,6 +10,7 @@ data class ApiSpace( val id: String = UUID.randomUUID().toString(), val admin_id: String = "", val name: String = "", + val encryptedSenderKeys: Map> = emptyMap(), // User-specific encrypted keys val created_at: Long? = System.currentTimeMillis() ) diff --git a/data/src/main/java/com/canopas/yourspace/data/models/user/ApiUser.kt b/data/src/main/java/com/canopas/yourspace/data/models/user/ApiUser.kt index e35e5e98..c00be17a 100644 --- a/data/src/main/java/com/canopas/yourspace/data/models/user/ApiUser.kt +++ b/data/src/main/java/com/canopas/yourspace/data/models/user/ApiUser.kt @@ -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 @@ -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? = 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" @@ -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, + val registrationId: Int +) + @Keep @JsonClass(generateAdapter = true) data class ApiUserSession( diff --git a/data/src/main/java/com/canopas/yourspace/data/security/entity/BaseEncryptedEntity.kt b/data/src/main/java/com/canopas/yourspace/data/security/entity/BaseEncryptedEntity.kt new file mode 100644 index 00000000..5989dda5 --- /dev/null +++ b/data/src/main/java/com/canopas/yourspace/data/security/entity/BaseEncryptedEntity.kt @@ -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 +) diff --git a/data/src/main/java/com/canopas/yourspace/data/security/helper/Helper.kt b/data/src/main/java/com/canopas/yourspace/data/security/helper/Helper.kt new file mode 100644 index 00000000..2d257ebd --- /dev/null +++ b/data/src/main/java/com/canopas/yourspace/data/security/helper/Helper.kt @@ -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) + } +} diff --git a/data/src/main/java/com/canopas/yourspace/data/security/helper/SignalKeyHelper.kt b/data/src/main/java/com/canopas/yourspace/data/security/helper/SignalKeyHelper.kt new file mode 100644 index 00000000..02c2a8e8 --- /dev/null +++ b/data/src/main/java/com/canopas/yourspace/data/security/helper/SignalKeyHelper.kt @@ -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 { + val records: MutableList = 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 { + 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 + ): Map> { + val encryptedKeys = mutableMapOf>() + 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) + } +} diff --git a/data/src/main/java/com/canopas/yourspace/data/security/session/EncryptedSpaceSession.kt b/data/src/main/java/com/canopas/yourspace/data/security/session/EncryptedSpaceSession.kt new file mode 100644 index 00000000..27b9eb40 --- /dev/null +++ b/data/src/main/java/com/canopas/yourspace/data/security/session/EncryptedSpaceSession.kt @@ -0,0 +1,110 @@ +package com.canopas.yourspace.data.security.session + +import android.util.Base64 +import androidx.annotation.Keep +import com.canopas.yourspace.data.models.user.ApiUser +import com.canopas.yourspace.data.security.helper.Helper +import com.squareup.moshi.JsonClass +import org.signal.libsignal.protocol.IdentityKey +import org.signal.libsignal.protocol.IdentityKeyPair +import org.signal.libsignal.protocol.NoSessionException +import org.signal.libsignal.protocol.SignalProtocolAddress +import org.signal.libsignal.protocol.ecc.Curve +import org.signal.libsignal.protocol.groups.GroupCipher +import org.signal.libsignal.protocol.groups.GroupSessionBuilder +import org.signal.libsignal.protocol.groups.state.SenderKeyRecord +import org.signal.libsignal.protocol.message.SenderKeyDistributionMessage +import org.signal.libsignal.protocol.state.PreKeyRecord +import org.signal.libsignal.protocol.state.SignedPreKeyRecord +import org.signal.libsignal.protocol.state.impl.InMemorySignalProtocolStore +import java.nio.charset.StandardCharsets +import java.util.UUID + +class EncryptedSpaceSession( + currentUser: ApiUser, + keyRecord: String?, + spaceId: String +) { + private val spaceDistributionId: UUID = 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 + } + private val signalProtocolAddress = SignalProtocolAddress(currentUser.id, 1) + private val identityKeyPair = IdentityKeyPair( + IdentityKey(Curve.decodePoint(Base64.decode(currentUser.public_key, Base64.DEFAULT), 0)), + Curve.decodePrivatePoint(Base64.decode(currentUser.private_key, Base64.DEFAULT)) + ) + + private val protocolStore = InMemorySignalProtocolStore(identityKeyPair, currentUser.registration_id) + private val encryptGroupCipher: GroupCipher + private val decryptCiphers = mutableMapOf() + private val distributionStore = mutableMapOf() + private val sessionBuilder = GroupSessionBuilder(protocolStore) + + init { + val signedPreKey = SignedPreKeyRecord(Helper.decodeToByteArray(currentUser.signed_pre_key)) + protocolStore.storeSignedPreKey(signedPreKey.id, signedPreKey) + + currentUser.pre_keys?.forEach { preKey -> + val record = PreKeyRecord(Helper.decodeToByteArray(preKey)) + protocolStore.storePreKey(record.id, record) + } + + if (keyRecord != null) { + protocolStore.storeSenderKey( + signalProtocolAddress, + spaceDistributionId, + SenderKeyRecord(Helper.decodeToByteArray(keyRecord)) + ) + } + + encryptGroupCipher = GroupCipher(protocolStore, signalProtocolAddress) + } + + val keyRecord: String + get() { + val record = protocolStore.loadSenderKey(signalProtocolAddress, spaceDistributionId) + return Helper.encodeToBase64(record.serialize()) + } + + fun createSession(members: List) { + members.forEach { member -> + val distributionKey = Helper.decodeToByteArray(member.keyDistributionMessage) + val keyMessage = SenderKeyDistributionMessage(distributionKey) + val address = SignalProtocolAddress(member.userId, 1) + + if (!decryptCiphers.containsKey(member.userId) || + distributionStore[member.userId] != member.keyDistributionMessage + ) { + distributionStore.remove(member.userId) + decryptCiphers.remove(member.userId) + + sessionBuilder.process(address, keyMessage) + distributionStore[member.userId] = member.keyDistributionMessage + decryptCiphers[member.userId] = GroupCipher(protocolStore, address) + } + } + } + + fun encryptMessage(message: String): String { + val encrypted = encryptGroupCipher.encrypt( + spaceDistributionId, + message.toByteArray(StandardCharsets.UTF_8) + ) + return Helper.encodeToBase64(encrypted.serialize()) + } + + fun decryptMessage(encryptedMessage: String?, userId: String): String { + val cipher = decryptCiphers[userId] ?: throw NoSessionException("No cipher for user $userId") + val decrypted = cipher.decrypt(Helper.decodeToByteArray(encryptedMessage)) + return String(decrypted, StandardCharsets.UTF_8) + } +} + +@Keep +@JsonClass(generateAdapter = false) +data class SpaceKeyDistribution( + val userId: String, + val keyDistributionMessage: String +) diff --git a/data/src/main/java/com/canopas/yourspace/data/service/auth/AuthService.kt b/data/src/main/java/com/canopas/yourspace/data/service/auth/AuthService.kt index 311e74b5..0923b2e2 100644 --- a/data/src/main/java/com/canopas/yourspace/data/service/auth/AuthService.kt +++ b/data/src/main/java/com/canopas/yourspace/data/service/auth/AuthService.kt @@ -75,7 +75,7 @@ class AuthService @Inject constructor( userPreferences.currentUser = newUser } - private var currentUserSession: ApiUserSession? + var currentUserSession: ApiUserSession? get() { return userPreferences.currentUserSession } diff --git a/data/src/main/java/com/canopas/yourspace/data/service/location/ApiJourneyService.kt b/data/src/main/java/com/canopas/yourspace/data/service/location/ApiJourneyService.kt index 3387fcc2..32b59605 100644 --- a/data/src/main/java/com/canopas/yourspace/data/service/location/ApiJourneyService.kt +++ b/data/src/main/java/com/canopas/yourspace/data/service/location/ApiJourneyService.kt @@ -1,8 +1,20 @@ package com.canopas.yourspace.data.service.location +import com.canopas.yourspace.data.models.location.EncryptedJourneyRoute +import com.canopas.yourspace.data.models.location.EncryptedLocationJourney import com.canopas.yourspace.data.models.location.JourneyRoute import com.canopas.yourspace.data.models.location.LocationJourney +import com.canopas.yourspace.data.models.space.ApiSpace +import com.canopas.yourspace.data.models.space.ApiSpaceMember +import com.canopas.yourspace.data.models.user.ApiUser +import com.canopas.yourspace.data.security.helper.SignalKeyHelper +import com.canopas.yourspace.data.security.session.EncryptedSpaceSession +import com.canopas.yourspace.data.security.session.SpaceKeyDistribution +import com.canopas.yourspace.data.service.user.ApiUserService +import com.canopas.yourspace.data.storage.UserPreferences import com.canopas.yourspace.data.utils.Config +import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_SPACES +import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_SPACE_MEMBERS import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.Query import com.google.firebase.firestore.toObject @@ -13,16 +25,26 @@ import javax.inject.Singleton @Singleton class ApiJourneyService @Inject constructor( - db: FirebaseFirestore + db: FirebaseFirestore, + private val userPreferences: UserPreferences, + private val signalKeyHelper: SignalKeyHelper, + private val apiUserService: ApiUserService ) { - private val userRef = db.collection(Config.FIRESTORE_COLLECTION_USERS) + var currentSpaceId: String = userPreferences.currentSpace ?: "" // App crashes sometimes because of the empty userId string passed to document(). // java.lang.IllegalArgumentException: Invalid document reference. // Document references must have an even number of segments, but users has 1 // https://stackoverflow.com/a/51195713/22508023 [Explanation can be found in comments] - private fun journeyRef(userId: String) = - userRef.document(userId.takeIf { it.isNotBlank() } ?: "null") + private val spaceRef = db.collection(FIRESTORE_COLLECTION_SPACES) + private fun spaceMemberRef(spaceId: String) = + spaceRef.document(spaceId.takeIf { it.isNotBlank() } ?: "null").collection( + FIRESTORE_COLLECTION_SPACE_MEMBERS + ) + + private fun spaceMemberJourneyRef(spaceId: String, userId: String) = + spaceMemberRef(spaceId) + .document(userId.takeIf { it.isNotBlank() } ?: "null") .collection(Config.FIRESTORE_COLLECTION_USER_JOURNEYS) suspend fun saveCurrentJourney( @@ -38,39 +60,103 @@ class ApiJourneyService @Inject constructor( updateAt: Long? = null, newJourneyId: ((String) -> Unit)? = null ) { - val docRef = journeyRef(userId).document() - - val journey = LocationJourney( - id = docRef.id, - user_id = userId, - from_latitude = fromLatitude, - from_longitude = fromLongitude, - to_latitude = toLatitude, - to_longitude = toLongitude, - route_distance = routeDistance, - route_duration = routeDuration, - routes = routes, - created_at = createdAt ?: System.currentTimeMillis(), - update_at = updateAt ?: System.currentTimeMillis() - ) + val user = userPreferences.currentUser ?: return + val userDeviceId = userPreferences.currentUserSession?.device_id ?: return + userPreferences.currentUser?.space_ids?.forEach { spaceId -> + val (_, senderKeyRecord) = signalKeyHelper.createDistributionKey( + user = user, + deviceId = userDeviceId, + spaceId = spaceId + ) + val spaceSession = EncryptedSpaceSession( + currentUser = user, + keyRecord = senderKeyRecord, + spaceId = spaceId + ) + val spaceMembers = spaceMemberRef(spaceId).get().await().toObjects(ApiSpaceMember::class.java) + val mSenderKeyDistributionModel = ArrayList().apply { + spaceMembers.forEach { member -> + val memberUser = apiUserService.getUser(member.user_id) ?: return + val decryptedSenderKey = getDecryptedSenderKey( + spaceId, + memberUser, + memberUser.public_key!! + ) + add( + SpaceKeyDistribution( + member.user_id, + decryptedSenderKey + ) + ) + } + } + spaceSession.createSession(mSenderKeyDistributionModel) + val encryptedFromLatitude = spaceSession.encryptMessage(fromLatitude.toString()) + val encryptedFromLongitude = spaceSession.encryptMessage(fromLongitude.toString()) + val encryptedToLatitude = toLatitude?.let { spaceSession.encryptMessage(it.toString()) } + val encryptedToLongitude = toLongitude?.let { spaceSession.encryptMessage(it.toString()) } + val encryptedJourneyRoutes = routes.map { + EncryptedJourneyRoute( + encrypted_latitude = spaceSession.encryptMessage(it.latitude.toString()), + encrypted_longitude = spaceSession.encryptMessage(it.longitude.toString()) + ) + } + + val docRef = spaceMemberJourneyRef(spaceId, userId).document() + + val encryptedJourney = EncryptedLocationJourney( + id = docRef.id, + user_id = userId, + encrypted_from_latitude = encryptedFromLatitude, + encrypted_from_longitude = encryptedFromLongitude, + encrypted_to_latitude = encryptedToLatitude, + encrypted_to_longitude = encryptedToLongitude, + route_distance = routeDistance, + route_duration = routeDuration, + encrypted_routes = encryptedJourneyRoutes, + created_at = createdAt ?: System.currentTimeMillis(), + updated_at = updateAt ?: System.currentTimeMillis() + ) - newJourneyId?.invoke(journey.id) + newJourneyId?.invoke(encryptedJourney.id) - docRef.set(journey).await() + docRef.set(encryptedJourney).await() + } + } + + private suspend fun getDecryptedSenderKey( + spaceId: String, + recipient: ApiUser, + senderPublicKey: String + ): String { + val space = spaceRef.document(spaceId).get().await().toObject(ApiSpace::class.java) + ?: throw Exception("Space not found") + + val encryptedKeys = space.encryptedSenderKeys[recipient.id] + ?: throw Exception("No keys found for recipient") + + return signalKeyHelper.decryptSenderKey( + encryptedSenderKey = encryptedKeys["encryptedSenderKey"]!!, + encryptedAESKey = encryptedKeys["encryptedAESKey"]!!, + recipientPrivateKey = recipient.private_key!!, + senderPublicKey = senderPublicKey + ) } suspend fun updateLastLocationJourney(userId: String, journey: LocationJourney) { try { - journeyRef(userId).document(journey.id).set(journey).await() + userPreferences.currentUser?.space_ids?.forEach { + spaceMemberJourneyRef(it, userId).document(journey.id).set(journey).await() + } } catch (e: Exception) { Timber.e(e, "Error while updating last location journey") } } suspend fun getLastJourneyLocation(userId: String) = try { - journeyRef(userId).whereEqualTo("user_id", userId) + spaceMemberJourneyRef(currentSpaceId, userId).whereEqualTo("user_id", userId) .orderBy("created_at", Query.Direction.DESCENDING).limit(1) - .get().await().documents.firstOrNull()?.toObject(LocationJourney::class.java) + .get().await().documents.firstOrNull()?.toObject() } catch (e: Exception) { Timber.e(e, "Error while getting last location journey") null @@ -80,17 +166,17 @@ class ApiJourneyService @Inject constructor( userId: String, from: Long? ): List { - if (from == null) { - return journeyRef(userId).whereEqualTo("user_id", userId) + val query = if (from == null) { + spaceMemberJourneyRef(currentSpaceId, userId).whereEqualTo("user_id", userId) + .orderBy("created_at", Query.Direction.DESCENDING) + .limit(20) + } else { + spaceMemberJourneyRef(currentSpaceId, userId).whereEqualTo("user_id", userId) .orderBy("created_at", Query.Direction.DESCENDING) + .whereLessThan("created_at", from) .limit(20) - .get().await().documents.mapNotNull { it.toObject() } } - return journeyRef(userId).whereEqualTo("user_id", userId) - .orderBy("created_at", Query.Direction.DESCENDING) - .whereLessThan("created_at", from) - .limit(20) - .get().await().documents.mapNotNull { it.toObject() } + return query.get().await().documents.mapNotNull { it.toObject() } } suspend fun getJourneyHistory( @@ -98,13 +184,13 @@ class ApiJourneyService @Inject constructor( from: Long, to: Long ): List { - val previousDayJourney = journeyRef(userId).whereEqualTo("user_id", userId) + val previousDayJourney = spaceMemberJourneyRef(currentSpaceId, userId).whereEqualTo("user_id", userId) .whereLessThan("created_at", from) .whereGreaterThanOrEqualTo("update_at", from) .limit(1) .get().await().documents.mapNotNull { it.toObject() } - val currentDayJourney = journeyRef(userId).whereEqualTo("user_id", userId) + val currentDayJourney = spaceMemberJourneyRef(currentSpaceId, userId).whereEqualTo("user_id", userId) .whereGreaterThanOrEqualTo("created_at", from) .whereLessThanOrEqualTo("created_at", to) .orderBy("created_at", Query.Direction.DESCENDING) @@ -114,8 +200,8 @@ class ApiJourneyService @Inject constructor( return previousDayJourney + currentDayJourney } - suspend fun getLocationJourneyFromId(userId: String, journeyId: String): LocationJourney? { - return journeyRef(userId).document(journeyId).get().await() + suspend fun getLocationJourneyFromId(journeyId: String, userId: String): LocationJourney? { + return spaceMemberJourneyRef(currentSpaceId, userId = userId).document(journeyId).get().await() .toObject(LocationJourney::class.java) } } diff --git a/data/src/main/java/com/canopas/yourspace/data/service/location/ApiLocationService.kt b/data/src/main/java/com/canopas/yourspace/data/service/location/ApiLocationService.kt index 41999c99..e7c76b30 100644 --- a/data/src/main/java/com/canopas/yourspace/data/service/location/ApiLocationService.kt +++ b/data/src/main/java/com/canopas/yourspace/data/service/location/ApiLocationService.kt @@ -1,7 +1,18 @@ package com.canopas.yourspace.data.service.location import com.canopas.yourspace.data.models.location.ApiLocation +import com.canopas.yourspace.data.models.location.EncryptedApiLocation +import com.canopas.yourspace.data.models.space.ApiSpace +import com.canopas.yourspace.data.models.space.ApiSpaceMember +import com.canopas.yourspace.data.models.user.ApiUser +import com.canopas.yourspace.data.security.helper.SignalKeyHelper +import com.canopas.yourspace.data.security.session.EncryptedSpaceSession +import com.canopas.yourspace.data.security.session.SpaceKeyDistribution +import com.canopas.yourspace.data.storage.UserPreferences import com.canopas.yourspace.data.utils.Config +import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_SPACES +import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_SPACE_MEMBERS +import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_USERS import com.canopas.yourspace.data.utils.snapshotFlow import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.Query @@ -15,28 +26,81 @@ import javax.inject.Singleton @Singleton class ApiLocationService @Inject constructor( db: FirebaseFirestore, - private val locationManager: LocationManager + private val locationManager: LocationManager, + private val userPreferences: UserPreferences, + private val signalKeyHelper: SignalKeyHelper ) { - private val userRef = db.collection(Config.FIRESTORE_COLLECTION_USERS) - private fun locationRef(userId: String) = - userRef.document(userId.takeIf { it.isNotBlank() } ?: "null") + var currentSpaceId: String = userPreferences.currentSpace ?: "" + + private val spaceRef = db.collection(FIRESTORE_COLLECTION_SPACES) + private val userRef = db.collection(FIRESTORE_COLLECTION_USERS) + private fun spaceMemberRef(spaceId: String) = + spaceRef.document(spaceId.takeIf { it.isNotBlank() } ?: "null").collection( + FIRESTORE_COLLECTION_SPACE_MEMBERS + ) + private fun spaceMemberLocationRef(spaceId: String, userId: String) = + spaceMemberRef(spaceId) + .document(userId.takeIf { it.isNotBlank() } ?: "null") .collection(Config.FIRESTORE_COLLECTION_USER_LOCATIONS) suspend fun saveLastKnownLocation( userId: String ) { val lastLocation = locationManager.getLastLocation() ?: return - val docRef = locationRef(userId).document() - - val location = ApiLocation( - id = docRef.id, - user_id = userId, - latitude = lastLocation.latitude, - longitude = lastLocation.longitude, - created_at = System.currentTimeMillis() - ) + val user = userPreferences.currentUser ?: return + val userDeviceId = userPreferences.currentUserSession?.device_id ?: return + userPreferences.currentUser?.space_ids?.forEach { spaceId -> + val (_, senderKeyRecord) = signalKeyHelper.createDistributionKey( + user = user, + deviceId = userDeviceId, + spaceId = spaceId + ) + val spaceSession = EncryptedSpaceSession( + currentUser = user, + keyRecord = senderKeyRecord, + spaceId = spaceId + ) + val spaceMembers = spaceMemberRef(spaceId).get().await().toObjects(ApiSpaceMember::class.java) + val mSenderKeyDistributionModel = ArrayList().apply { + spaceMembers.forEach { member -> + val memberUser = getUser(member.user_id) ?: return + val decryptedSenderKey = getDecryptedSenderKey( + spaceId, + memberUser, + memberUser.public_key!! + ) + add( + SpaceKeyDistribution( + member.user_id, + decryptedSenderKey + ) + ) + } + } + spaceSession.createSession(mSenderKeyDistributionModel) + val encryptedLatitude = spaceSession.encryptMessage(lastLocation.latitude.toString()) + val encryptedLongitude = spaceSession.encryptMessage(lastLocation.longitude.toString()) + val docRef = spaceMemberLocationRef(spaceId, userId).document() - docRef.set(location).await() + val encryptedLocation = EncryptedApiLocation( + id = docRef.id, + user_id = userId, + encrypted_latitude = encryptedLatitude, + encrypted_longitude = encryptedLongitude, + created_at = System.currentTimeMillis() + ) + + docRef.set(encryptedLocation).await() + } + } + + suspend fun getUser(userId: String): ApiUser? { + return try { + userRef.document(userId).get().await().toObject(ApiUser::class.java) + } catch (e: Exception) { + Timber.e(e, "Error while getting user") + null + } } suspend fun saveCurrentLocation( @@ -45,57 +109,119 @@ class ApiLocationService @Inject constructor( longitude: Double, recordedAt: Long ) { - val docRef = locationRef(userId).document() - - val location = ApiLocation( - id = docRef.id, - user_id = userId, - latitude = latitude, - longitude = longitude, - created_at = recordedAt - ) + val user = userPreferences.currentUser ?: return + val userDeviceId = userPreferences.currentUserSession?.device_id ?: return + userPreferences.currentUser?.space_ids?.forEach { spaceId -> + val (_, senderKeyRecord) = signalKeyHelper.createDistributionKey( + user = user, + deviceId = userDeviceId, + spaceId = spaceId + ) + val spaceSession = EncryptedSpaceSession( + currentUser = user, + keyRecord = senderKeyRecord, + spaceId = spaceId + ) + val spaceMembers = spaceMemberRef(spaceId).get().await().toObjects(ApiSpaceMember::class.java) + val mSenderKeyDistributionModel = ArrayList().apply { + spaceMembers.forEach { member -> + val memberUser = getUser(member.user_id) ?: return + val decryptedSenderKey = getDecryptedSenderKey( + spaceId, + memberUser, + memberUser.public_key!! + ) + add( + SpaceKeyDistribution( + member.user_id, + decryptedSenderKey + ) + ) + } + } + spaceSession.createSession(mSenderKeyDistributionModel) + val encryptedLatitude = spaceSession.encryptMessage(latitude.toString()) + val encryptedLongitude = spaceSession.encryptMessage(longitude.toString()) + val docRef = spaceMemberLocationRef(spaceId, userId).document() - docRef.set(location).await() - } + val encryptedLocation = EncryptedApiLocation( + id = docRef.id, + user_id = userId, + encrypted_latitude = encryptedLatitude, + encrypted_longitude = encryptedLongitude, + created_at = recordedAt + ) - fun getCurrentLocation(userId: String): Flow>? { - return try { - locationRef(userId).whereEqualTo("user_id", userId) - .orderBy("created_at", Query.Direction.DESCENDING).limit(1) - .snapshotFlow(ApiLocation::class.java) - } catch (e: Exception) { - Timber.e(e, "Error while getting current location") - null + docRef.set(encryptedLocation).await() } } - suspend fun getLastFiveMinuteLocations(userId: String): Flow> { - val currentTime = System.currentTimeMillis() - val locations = mutableListOf() + private suspend fun getDecryptedSenderKey( + spaceId: String, + recipient: ApiUser, + senderPublicKey: String + ): String { + val space = spaceRef.document(spaceId).get().await().toObject(ApiSpace::class.java) + ?: throw Exception("Space not found") - for (i in 0 until 5) { + val encryptedKeys = space.encryptedSenderKeys[recipient.id] + ?: throw Exception("No keys found for recipient") + + return signalKeyHelper.decryptSenderKey( + encryptedSenderKey = encryptedKeys["encryptedSenderKey"]!!, + encryptedAESKey = encryptedKeys["encryptedAESKey"]!!, + recipientPrivateKey = recipient.private_key!!, + senderPublicKey = senderPublicKey + ) + } + + fun getCurrentLocation(userId: String): Flow> { + return flow { try { - val startTime = currentTime - (i + 1) * 60000 - val endTime = startTime - 60000 - val reference = locationRef(userId) - val apiLocation = reference + val encryptedLocation = spaceMemberLocationRef(currentSpaceId, userId) .whereEqualTo("user_id", userId) - .whereGreaterThanOrEqualTo("created_at", endTime) - .whereLessThan("created_at", startTime) - .orderBy("created_at", Query.Direction.DESCENDING).limit(1) - .get().await().documents - .firstOrNull()?.toObject(ApiLocation::class.java) - - apiLocation?.let { - locations.add(it) + .orderBy("created_at", Query.Direction.DESCENDING) + .limit(1) + .snapshotFlow(EncryptedApiLocation::class.java) + + encryptedLocation.collect { encryptedLocationList -> + val apiLocations = encryptedLocationList.mapNotNull { encryptedLocation -> + val user = getUser(encryptedLocation.user_id) ?: return@mapNotNull null + val senderPublicKey = user.public_key ?: return@mapNotNull null + val space = spaceRef.document(currentSpaceId).get().await().toObject(ApiSpace::class.java) + ?: throw Exception("Space not found") + + val encryptedKeys = space.encryptedSenderKeys[user.id] + ?: throw Exception("No keys found for recipient") + + val decryptedSenderKey = signalKeyHelper.decryptSenderKey( + encryptedSenderKey = encryptedKeys["encryptedSenderKey"]!!, + encryptedAESKey = encryptedKeys["encryptedAESKey"]!!, + recipientPrivateKey = user.private_key!!, + senderPublicKey = senderPublicKey + ) + + val spaceSession = EncryptedSpaceSession( + currentUser = user, + keyRecord = decryptedSenderKey, + spaceId = currentSpaceId + ) + val decryptedLatitude = spaceSession.decryptMessage(encryptedLocation.encrypted_latitude, user.id) + val decryptedLongitude = spaceSession.decryptMessage(encryptedLocation.encrypted_longitude, user.id) + + ApiLocation( + user_id = user.id, + latitude = decryptedLatitude.toDouble(), + longitude = decryptedLongitude.toDouble() + ) + } + + emit(apiLocations) // Emit the list of ApiLocation } } catch (e: Exception) { - Timber.e(e, "Error while getting last $i minute locations") + Timber.e(e, "Error while getting current location") + emit(emptyList()) // Emit an empty list in case of an error } } - - return flow { - emit(locations) - } } } diff --git a/data/src/main/java/com/canopas/yourspace/data/service/space/ApiSpaceService.kt b/data/src/main/java/com/canopas/yourspace/data/service/space/ApiSpaceService.kt index c20f7fc2..ab0f19ce 100644 --- a/data/src/main/java/com/canopas/yourspace/data/service/space/ApiSpaceService.kt +++ b/data/src/main/java/com/canopas/yourspace/data/service/space/ApiSpaceService.kt @@ -4,6 +4,8 @@ import com.canopas.yourspace.data.models.space.ApiSpace import com.canopas.yourspace.data.models.space.ApiSpaceMember import com.canopas.yourspace.data.models.space.SPACE_MEMBER_ROLE_ADMIN import com.canopas.yourspace.data.models.space.SPACE_MEMBER_ROLE_MEMBER +import com.canopas.yourspace.data.models.user.ApiUser +import com.canopas.yourspace.data.security.helper.SignalKeyHelper import com.canopas.yourspace.data.service.auth.AuthService import com.canopas.yourspace.data.service.place.ApiPlaceService import com.canopas.yourspace.data.service.user.ApiUserService @@ -11,6 +13,8 @@ import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_SPACES import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_SPACE_MEMBERS import com.canopas.yourspace.data.utils.snapshotFlow import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton @@ -20,7 +24,8 @@ class ApiSpaceService @Inject constructor( private val db: FirebaseFirestore, private val authService: AuthService, private val apiUserService: ApiUserService, - private val placeService: ApiPlaceService + private val placeService: ApiPlaceService, + private val signalKeyHelper: SignalKeyHelper ) { private val spaceRef = db.collection(FIRESTORE_COLLECTION_SPACES) private fun spaceMemberRef(spaceId: String) = @@ -49,9 +54,45 @@ class ApiSpaceService @Inject constructor( it.set(member).await() } + val user = authService.currentUser ?: return + val userDeviceId = authService.currentUserSession?.device_id ?: return + val (senderKey, _) = signalKeyHelper.createDistributionKey( + user = user, + deviceId = userDeviceId, + spaceId = spaceId + ) + val spaceMembers = getMemberBySpaceId(spaceId).firstOrNull()?.map { + apiUserService.getUser(it.user_id) ?: return@map null + } + distributeSenderKeyToGroup( + spaceId = spaceId, + senderKey = senderKey, + senderPrivateKey = user.private_key!!, + members = spaceMembers ?: emptyList() + ) apiUserService.addSpaceId(userId, spaceId) } + private suspend fun distributeSenderKeyToGroup( + spaceId: String, + senderKey: String, + senderPrivateKey: String, + members: List + ) { + // Encrypt the Sender Key for each recipient + val encryptedKeys = signalKeyHelper.encryptSenderKeyForGroup(senderKey, senderPrivateKey, members) + val encryptedSenderKeysMap = encryptedKeys.mapValues { entry -> + mapOf( + "encryptedSenderKey" to entry.value.first, + "encryptedAESKey" to entry.value.second + ) + } + + spaceRef.document(spaceId) + .update("encryptedSenderKeys", encryptedSenderKeysMap) + .await() + } + suspend fun enableLocation(spaceId: String, userId: String, enable: Boolean) { spaceMemberRef(spaceId) .whereEqualTo("user_id", userId).get() @@ -95,6 +136,22 @@ class ApiSpaceService @Inject constructor( .whereEqualTo("user_id", userId).get().await().documents.forEach { it.reference.delete().await() } + val user = authService.currentUser ?: return + val userDeviceId = authService.currentUserSession?.device_id ?: return + val (senderKey, _) = signalKeyHelper.createDistributionKey( + user = user, + deviceId = userDeviceId, + spaceId = spaceId + ) + val spaceMembers = getMemberBySpaceId(spaceId).firstOrNull()?.map { + apiUserService.getUser(it.user_id) ?: return@map null + } + distributeSenderKeyToGroup( + spaceId = spaceId, + senderKey = senderKey, + senderPrivateKey = user.private_key!!, + members = spaceMembers ?: emptyList() + ) } suspend fun updateSpace(space: ApiSpace) { diff --git a/data/src/main/java/com/canopas/yourspace/data/service/user/ApiUserService.kt b/data/src/main/java/com/canopas/yourspace/data/service/user/ApiUserService.kt index 9f52f4ab..032eb8fb 100644 --- a/data/src/main/java/com/canopas/yourspace/data/service/user/ApiUserService.kt +++ b/data/src/main/java/com/canopas/yourspace/data/service/user/ApiUserService.kt @@ -4,6 +4,8 @@ import com.canopas.yourspace.data.models.user.ApiUser import com.canopas.yourspace.data.models.user.ApiUserSession import com.canopas.yourspace.data.models.user.LOGIN_TYPE_APPLE import com.canopas.yourspace.data.models.user.LOGIN_TYPE_GOOGLE +import com.canopas.yourspace.data.security.helper.Helper +import com.canopas.yourspace.data.security.helper.SignalKeyHelper import com.canopas.yourspace.data.service.location.ApiLocationService import com.canopas.yourspace.data.utils.Config import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_USERS @@ -29,6 +31,7 @@ class ApiUserService @Inject constructor( db: FirebaseFirestore, private val device: Device, private val locationService: ApiLocationService, + private val signalKeyHelper: SignalKeyHelper, private val functions: FirebaseFunctions ) { private val userRef = db.collection(FIRESTORE_COLLECTION_USERS) @@ -74,6 +77,12 @@ class ApiUserService @Inject constructor( sessionDocRef.set(session).await() return Triple(false, savedUser, session) } else { + val signalKeys = signalKeyHelper.generateSignalKeys() + val publicKey = Helper.encodeToBase64(signalKeys.identityKeyPair.publicKey.serialize()) + val privateKey = Helper.encodeToBase64(signalKeys.identityKeyPair.privateKey.serialize()) + val preKeys = signalKeys.preKeys.map { Helper.encodeToBase64(it.serialize()) } + val signedPreKey = Helper.encodeToBase64(signalKeys.signedPreKey.serialize()) + val user = ApiUser( id = uid!!, email = account?.email ?: firebaseUser?.email ?: "", @@ -82,7 +91,12 @@ class ApiUserService @Inject constructor( last_name = account?.familyName ?: "", provider_firebase_id_token = firebaseToken, profile_image = account?.photoUrl?.toString() ?: firebaseUser?.photoUrl?.toString() - ?: "" + ?: "", + public_key = publicKey, + private_key = privateKey, + pre_keys = preKeys, + signed_pre_key = signedPreKey, + registration_id = signalKeys.registrationId ) userRef.document(uid).set(user).await() val sessionDocRef = sessionRef(user.id).document() diff --git a/firestore.rules b/firestore.rules index d9306cd5..fb0c0c18 100644 --- a/firestore.rules +++ b/firestore.rules @@ -1,263 +1,280 @@ rules_version = '2'; service cloud.firestore { - match /databases/{database}/documents { + match /databases/{database}/documents { - function isAuthorized() { - return request.auth != null; - } - - function readUserLocation() { - let requestedUserSpaceIds = get(/databases/$(database)/documents/users/$(request.auth.uid)).data.space_ids; - let resourceUserSpaceIds = get(/databases/$(database)/documents/users/$(resource.data.user_id)).data.space_ids; - return requestedUserSpaceIds.hasAny(resourceUserSpaceIds); - } - - match /support_requests/{docId} { - allow create : if isAuthorized() && request.auth.uid == request.resource.data.user_id && - request.resource.data.keys().hasAll(["user_id", "title", "description", "device_name","app_version","device_os", "created_at"]) && - request.resource.data.user_id is string && - request.resource.data.title is string && - request.resource.data.description is string && - request.resource.data.device_name is string && - request.resource.data.app_version is string && - request.resource.data.device_os is string && - request.resource.data.created_at is timestamp && - request.resource.data.get('attachments', []) is list; - allow update: if false; - allow delete: if false; - allow read: if isAuthorized() && request.auth.uid == resource.data.user_id; - } - - match /users/{docId} { - allow create : if isAuthorized() && request.auth.uid == docId && - request.resource.data.keys().hasAll(["id", "auth_type", "location_enabled", "provider_firebase_id_token", "created_at"]) && - request.resource.data.keys().hasAny(["email", "phone"]) && - request.resource.data.id is string && - request.resource.data.auth_type is int && - (request.resource.data.auth_type == 1 || request.resource.data.auth_type == 2) && - request.resource.data.location_enabled is bool && - request.resource.data.provider_firebase_id_token is string && - request.resource.data.created_at is int && - request.resource.data.get('first_name', '') is string && - request.resource.data.get('phone', '') is string && - request.resource.data.get('email', '') is string && - request.resource.data.get('last_name', '') is string && - request.resource.data.get('fcm_token', '') is string && - request.resource.data.get('profile_image', '') is string && - request.resource.data.get('space_ids', []) is list; - - allow update: if isAuthorized() && request.auth.uid == resource.data.id && - request.resource.data.diff(resource.data).affectedKeys().hasAny(['first_name', 'last_name', 'profile_image', 'location_enabled', 'space_ids', 'phone', 'email','fcm_token', 'updated_at', 'battery_pct', 'state']) && - request.resource.data.first_name is string && - request.resource.data.get('last_name', '') is string && - request.resource.data.get('fcm_token', '') is string && - request.resource.data.location_enabled is bool && - request.resource.data.get('space_ids', []) is list; + function isAuthorized() { + return request.auth != null; + } - allow delete: if isAuthorized() && request.auth.uid == resource.data.id; - allow read: if isAuthorized(); + function readUserLocation() { + let requestedUserSpaceIds = get(/databases/$(database)/documents/users/$(request.auth.uid)).data.space_ids; + let resourceUserSpaceIds = get(/databases/$(database)/documents/users/$(resource.data.user_id)).data.space_ids; + return requestedUserSpaceIds.hasAny(resourceUserSpaceIds); + } - match /user_locations/{docId} { - allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || readUserLocation()); - allow update: if isAuthorized() && request.auth.uid == resource.data.user_id; - allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; - allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && - request.resource.data.keys().hasAll(["id", "user_id", "latitude", "longitude", "created_at"]) && - request.resource.data.id is string && - request.resource.data.user_id is string && - request.resource.data.latitude is number && - request.resource.data.longitude is number && - request.resource.data.created_at is int; - } + match /support_requests/{docId} { + allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && + request.resource.data.keys().hasAll(["user_id", "title", "description", "device_name", "app_version", "device_os", "created_at"]) && + request.resource.data.user_id is string && + request.resource.data.title is string && + request.resource.data.description is string && + request.resource.data.device_name is string && + request.resource.data.app_version is string && + request.resource.data.device_os is string && + request.resource.data.created_at is timestamp && + request.resource.data.get('attachments', []) is list; + allow update: if false; + allow delete: if false; + allow read: if isAuthorized() && request.auth.uid == resource.data.user_id; + } - match /user_journeys/{docId} { - allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || readUserLocation()); - allow update: if true; - allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; - allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && - request.resource.data.keys().hasAll(["id", "user_id", "from_latitude", "from_longitude", "created_at"]) && - request.resource.data.id is string && - request.resource.data.user_id is string && - request.resource.data.from_latitude is number && - request.resource.data.from_longitude is number && - request.resource.data.created_at is int; - } + match /users/{docId} { + allow create: if isAuthorized() && request.auth.uid == docId && + request.resource.data.keys().hasAll(["id", "auth_type", "location_enabled", "provider_firebase_id_token", "created_at"]) && + request.resource.data.email is string && + request.resource.data.id is string && + request.resource.data.auth_type is int && + (request.resource.data.auth_type == 1 || request.resource.data.auth_type == 2) && + request.resource.data.location_enabled is bool && + request.resource.data.provider_firebase_id_token is string && + request.resource.data.created_at is int && + request.resource.data.get('first_name', '') is string && + request.resource.data.get('email', '') is string && + request.resource.data.get('last_name', '') is string && + request.resource.data.get('fcm_token', '') is string && + request.resource.data.get('profile_image', '') is string && + request.resource.data.get('space_ids', []) is list; + allow update: if true; + allow delete: if isAuthorized() && request.auth.uid == resource.data.id; + allow read: if isAuthorized(); + match /user_locations/{docId} { + allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || readUserLocation()); + allow update: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && + request.resource.data.keys().hasAll(["id", "user_id", "latitude", "longitude", "created_at"]) && + request.resource.data.id is string && + request.resource.data.user_id is string && + request.resource.data.latitude is number && + request.resource.data.longitude is number && + request.resource.data.created_at is int; + } - match /user_sessions/{docId} { - allow read: if isAuthorized(); - allow create : if isAuthorized() && request.auth.uid == request.resource.data.user_id && - request.resource.data.keys().hasAll(["id", "user_id", "device_id", "device_name", "platform", "session_active", "app_version", "created_at"]) && - request.resource.data.id is string && - request.resource.data.user_id is string && - request.resource.data.device_id is string && - request.resource.data.device_name is string && - request.resource.data.platform is int && - request.resource.data.platform == 1 && - request.resource.data.session_active is bool && - request.resource.data.app_version is int && - request.resource.data.created_at is int; - allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; - allow update: if isAuthorized() && request.auth.uid == resource.data.user_id; - } - } + match /user_journeys/{docId} { + allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || readUserLocation()); + allow update: if true; + allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && + request.resource.data.keys().hasAll(["id", "user_id", "from_latitude", "from_longitude", "created_at"]) && + request.resource.data.id is string && + request.resource.data.user_id is string && + request.resource.data.from_latitude is number && + request.resource.data.from_longitude is number && + request.resource.data.created_at is int; + } - match /users/{docId}/user_sessions/{document=**} { - allow read: if isAuthorized() && request.auth.uid == docId; - } + match /user_sessions/{docId} { + allow read: if isAuthorized(); + allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && + request.resource.data.keys().hasAll(["id", "user_id", "device_id", "device_name", "platform", "session_active", "app_version", "created_at"]) && + request.resource.data.id is string && + request.resource.data.user_id is string && + request.resource.data.device_id is string && + request.resource.data.device_name is string && + request.resource.data.platform is int && + request.resource.data.platform == 1 && + request.resource.data.session_active is bool && + request.resource.data.app_version is int && + request.resource.data.created_at is int; + allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow update: if isAuthorized() && request.auth.uid == resource.data.user_id; + } + } - function isSpaceAdmin(spaceId) { - let adminId = get(/databases/$(database)/documents/spaces/$(spaceId)).data.admin_id; - return request.auth.uid == adminId; - } + match /users/{docId}/user_sessions/{document=**} { + allow read: if isAuthorized() && request.auth.uid == docId; + } - function isSpaceMember(spaceId) { - let isMember = exists(/databases/$(database)/documents/spaces/$(spaceId)/space_members/$(request.auth.uid)); - return isMember; - } + function isSpaceAdmin(spaceId) { + let adminId = get(/databases/$(database)/documents/spaces/$(spaceId)).data.admin_id; + return request.auth.uid == adminId; + } - match /spaces/{docId} { - allow read: if true; - allow delete: if isAuthorized() && request.auth.uid == resource.data.admin_id; - allow update: if isAuthorized() && request.auth.uid == resource.data.admin_id; - allow create: if isAuthorized() && - request.resource.data.keys().hasAll(["id", "admin_id", "name", "created_at"]) && - request.resource.data.id is string && - request.resource.data.admin_id is string && - request.resource.data.name is string && - request.resource.data.created_at is int; + function isSpaceMember(spaceId) { + let isMember = exists(/databases/$(database)/documents/spaces/$(spaceId)/space_members/$(request.auth.uid)); + return isMember; + } + match /spaces/{docId} { + allow read: if true; + allow delete: if isAuthorized() && request.auth.uid == resource.data.admin_id; + allow update: if isAuthorized() && request.auth.uid == resource.data.admin_id; + allow create: if isAuthorized() && + request.resource.data.keys().hasAll(["id", "admin_id", "name", "created_at"]) && + request.resource.data.id is string && + request.resource.data.admin_id is string && + request.resource.data.name is string && + request.resource.data.created_at is int; + } - } - match /{path=**}/space_places/{place} { - allow read: if isAuthorized() && isSpaceMember(resource.data.space_id); - allow write: if false; - } + match /{path=**}/space_places/{place} { + allow read: if isAuthorized() && isSpaceMember(resource.data.space_id); + allow write: if false; + } - match /spaces/{spaceId}/space_places/{place} { - allow read: if isAuthorized() && isSpaceMember(spaceId); - allow delete: if isAuthorized() && (isSpaceAdmin(resource.data.space_id) || request.auth.uid == resource.data.created_by); - allow update: if isAuthorized() && request.auth.uid == resource.data.created_by; - allow create: if isAuthorized() && (isSpaceAdmin(request.resource.data.space_id) || request.auth.uid == request.resource.data.created_by) && - request.resource.data.keys().hasAll(["id", "space_id", "created_by", "latitude", "longitude", "radius", "name", "created_at"]) && - request.resource.data.id is string && - request.resource.data.space_id is string && - request.resource.data.created_by is string && - request.resource.data.latitude is number && - request.resource.data.longitude is number && - request.resource.data.radius is number && - request.resource.data.name is string && - request.resource.data.created_at is timestamp; - } + match /spaces/{spaceId}/space_places/{place} { + allow read: if isAuthorized() && isSpaceMember(spaceId); + allow delete: if isAuthorized() && (isSpaceAdmin(resource.data.space_id) || request.auth.uid == resource.data.created_by); + allow update: if isAuthorized() && request.auth.uid == resource.data.created_by; + allow create: if isAuthorized() && (isSpaceAdmin(request.resource.data.space_id) || request.auth.uid == request.resource.data.created_by) && + request.resource.data.keys().hasAll(["id", "space_id", "created_by", "latitude", "longitude", "radius", "name", "created_at"]) && + request.resource.data.id is string && + request.resource.data.space_id is string && + request.resource.data.created_by is string && + request.resource.data.latitude is number && + request.resource.data.longitude is number && + request.resource.data.radius is number && + request.resource.data.name is string && + request.resource.data.created_at is timestamp; + } - function isPlaceAdmin(spaceId, placeId) { - let created_by = get(/databases/$(database)/documents/spaces/$(spaceId)/space_places/$(place)).data.created_by; - return request.auth.uid == created_by; - } + function isPlaceAdmin(spaceId, place) { + let created_by = get(/databases/$(database)/documents/spaces/$(spaceId)/space_places/$(place)).data.created_by; + return request.auth.uid == created_by; + } - match /spaces/{spaceId}/space_places/{place}/place_settings_by_members/{member} { - allow read: if isAuthorized() && isSpaceMember(spaceId); - allow update: if isAuthorized() && isSpaceMember(spaceId) && - request.resource.data.diff(resource.data).affectedKeys().hasOnly(["alert_enable", "leave_alert_for", "arrival_alert_for"]) && - request.resource.data.arrival_alert_for is list && - request.resource.data.leave_alert_for is list && - request.resource.data.alert_enable is bool; - allow delete: if isAuthorized() && (request.auth.uid == resource.data.user_id || isPlaceAdmin(place)); - allow create: if isAuthorized() && isSpaceMember(spaceId) && - request.resource.data.keys().hasAll(["user_id", "place_id", "alert_enable", "leave_alert_for", "arrival_alert_for"]) && - request.resource.data.user_id is string && - request.resource.data.place_id is string && - request.resource.data.get('arrival_alert_for', []) is list && - request.resource.data.get('leave_alert_for', []) is list && - request.resource.data.alert_enable is bool; - } + match /spaces/{spaceId}/space_places/{place}/place_settings_by_members/{member} { + allow read: if isAuthorized() && isSpaceMember(spaceId); + allow update: if isAuthorized() && isSpaceMember(spaceId) && + request.resource.data.diff(resource.data).affectedKeys().hasOnly(["alert_enable", "leave_alert_for", "arrival_alert_for"]) && + request.resource.data.arrival_alert_for is list && + request.resource.data.leave_alert_for is list && + request.resource.data.alert_enable is bool; + allow delete: if isAuthorized() && (request.auth.uid == resource.data.user_id || isPlaceAdmin(spaceId, place)); + allow create: if isAuthorized() && isSpaceMember(spaceId) && + request.resource.data.keys().hasAll(["user_id", "place_id", "alert_enable", "leave_alert_for", "arrival_alert_for"]) && + request.resource.data.user_id is string && + request.resource.data.place_id is string && + request.resource.data.get('arrival_alert_for', []) is list && + request.resource.data.get('leave_alert_for', []) is list && + request.resource.data.alert_enable is bool; + } - match /{path=**}/space_members/{member} { - allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || isSpaceMember(resource.data.space_id)); - allow write: if false; - } + match /{path=**}/space_members/{member} { + allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || isSpaceMember(resource.data.space_id)); + allow write: if false; + } - match /spaces/{spaceId}/space_members/{member} { - allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || isSpaceMember(spaceId)); - allow delete: if isAuthorized() && (isSpaceAdmin(resource.data.space_id) || request.auth.uid == resource.data.user_id); - allow update: if isAuthorized() && request.auth.uid == resource.data.user_id && - request.resource.data.diff(resource.data).affectedKeys().hasOnly(["location_enabled"]) && - request.resource.data.location_enabled is bool; - allow create: if isAuthorized() && (isSpaceAdmin(request.resource.data.space_id) || request.auth.uid == request.resource.data.user_id) && - request.resource.data.keys().hasAll(["id", "space_id", "user_id", "role", "location_enabled", "created_at"]) && - request.resource.data.id is string && - request.resource.data.space_id is string && - request.resource.data.user_id is string && - request.resource.data.role is int && - (request.resource.data.role == 1 || request.resource.data.role == 2) && - request.resource.data.location_enabled is bool && - request.resource.data.created_at is int; - } + match /spaces/{spaceId}/space_members/{member} { + allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || isSpaceMember(spaceId)); + allow delete: if isAuthorized() && (isSpaceAdmin(resource.data.space_id) || request.auth.uid == resource.data.user_id); + allow update: if isAuthorized() && + (request.auth.uid == resource.data.user_id || isSpaceAdmin(resource.data.space_id)) && + request.resource.data.diff(resource.data).affectedKeys().hasOnly(["location_enabled", "role"]) && + ((request.resource.data.location_enabled is bool) || + (request.resource.data.role is int && (request.resource.data.role == 1 || request.resource.data.role == 2))); + allow create: if isAuthorized() && (isSpaceAdmin(request.resource.data.space_id) || request.auth.uid == request.resource.data.user_id) && + request.resource.data.keys().hasAll(["id", "space_id", "user_id", "role", "location_enabled", "created_at"]) && + request.resource.data.id is string && + request.resource.data.space_id is string && + request.resource.data.user_id is string && + request.resource.data.role is int && + (request.resource.data.role == 1 || request.resource.data.role == 2) && + request.resource.data.location_enabled is bool && + request.resource.data.created_at is int; + } - match /space_invitations/{docId} { - allow read: if isAuthorized(); - allow delete: if isAuthorized() && isSpaceAdmin(resource.data.space_id); - allow update: if isAuthorized() && isSpaceMember(resource.data.space_id) && - request.resource.data.diff(resource.data).affectedKeys().hasOnly(["code", "created_at"]) && - request.resource.data.code is string && - request.resource.data.code.size() == 6 && - request.resource.data.created_at is int; + match /spaces/{spaceId}/space_members/{docId} { + match /user_locations/{docId} { + allow read: if true; + allow update: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && + request.resource.data.keys().hasAll(["id", "user_id", "encrypted_latitude", "encrypted_longitude", "created_at"]) && + request.resource.data.id is string && + request.resource.data.user_id is string && + request.resource.data.encrypted_latitude is string && + request.resource.data.encrypted_longitude is string && + request.resource.data.created_at is int; + } - allow create: if isAuthorized() && isSpaceAdmin(request.resource.data.space_id) && - request.resource.data.keys().hasAll(["id", "code", "space_id", "created_at"]) && - request.resource.data.id is string && - request.resource.data.space_id is string && - request.resource.data.code is string && - request.resource.data.code.size() == 6 && - request.resource.data.created_at is int; - } + match /user_journeys/{docId} { + allow read: if isAuthorized() && (request.auth.uid == resource.data.user_id || readUserLocation()); + allow update: if true; + allow delete: if isAuthorized() && request.auth.uid == resource.data.user_id; + allow create: if isAuthorized() && request.auth.uid == request.resource.data.user_id && + request.resource.data.keys().hasAll(["id", "user_id", "encrypted_from_latitude", "encrypted_from_longitude", "created_at"]) && + request.resource.data.id is string && + request.resource.data.user_id is string && + request.resource.data.encrypted_from_latitude is string && + request.resource.data.encrypted_from_longitude is string && + request.resource.data.created_at is int; + } + } + match /space_invitations/{docId} { + allow read: if isAuthorized(); + allow delete: if isAuthorized() && isSpaceAdmin(resource.data.space_id); + allow update: if isAuthorized() && isSpaceMember(resource.data.space_id) && + request.resource.data.diff(resource.data).affectedKeys().hasOnly(["code", "created_at"]) && + request.resource.data.code is string && + request.resource.data.code.size() == 6 && + request.resource.data.created_at is int; + allow create: if isAuthorized() && isSpaceAdmin(request.resource.data.space_id) && + request.resource.data.keys().hasAll(["id", "code", "space_id", "created_at"]) && + request.resource.data.id is string && + request.resource.data.space_id is string && + request.resource.data.code is string && + request.resource.data.code.size() == 6 && + request.resource.data.created_at is int; + } - match /space_threads/{docId} { - allow read: if isAuthorized() && (isSpaceAdmin(resource.data.space_id) || isSpaceMember(resource.data.space_id)); - allow delete: if isAuthorized() && isThreadAdmin(docId); - allow update: if isAuthorized() && isThreadMember(docId) && - request.resource.data.diff(resource.data).affectedKeys().hasAny(["member_ids", "archived_for"]) && - request.resource.data.get('member_ids', []) is list && - request.resource.data.get('archived_for', {}) is map; - allow create: if isAuthorized() && (isSpaceMember(request.resource.data.space_id)) && - request.resource.data.keys().hasAll(["id", "space_id", "admin_id", "member_ids", "created_at"]) && - request.resource.data.id is string && - request.resource.data.space_id is string && - request.resource.data.admin_id is string && - request.resource.data.member_ids is list && - request.resource.data.created_at is int; + match /space_threads/{docId} { + allow read: if isAuthorized() && (isSpaceAdmin(resource.data.space_id) || isSpaceMember(resource.data.space_id)); + allow delete: if isAuthorized() && isThreadAdmin(docId); + allow update: if isAuthorized() && isThreadMember(docId) && + request.resource.data.diff(resource.data).affectedKeys().hasAny(["member_ids", "archived_for"]) && + request.resource.data.get('member_ids', []) is list && + request.resource.data.get('archived_for', {}) is map; + allow create: if isAuthorized() && (isSpaceMember(request.resource.data.space_id)) && + request.resource.data.keys().hasAll(["id", "space_id", "admin_id", "member_ids", "created_at"]) && + request.resource.data.id is string && + request.resource.data.space_id is string && + request.resource.data.admin_id is string && + request.resource.data.member_ids is list && + request.resource.data.created_at is int; + } - } - function isThreadMember(threadId) { - let memberIds = get(/databases/$(database)/documents/space_threads/$(threadId)).data.member_ids; - return memberIds.hasAny([request.auth.uid]); - } + function isThreadMember(threadId) { + let memberIds = get(/databases/$(database)/documents/space_threads/$(threadId)).data.member_ids; + return memberIds.hasAny([request.auth.uid]); + } - function isThreadAdmin(threadId) { - let adminId = get(/databases/$(database)/documents/space_threads/$(threadId)).data.admin_id; - return adminId == request.auth.uid; - } + function isThreadAdmin(threadId) { + let adminId = get(/databases/$(database)/documents/space_threads/$(threadId)).data.admin_id; + return adminId == request.auth.uid; + } - match /{path=**}/thread_messages/{docId} { - allow read: if isAuthorized() && isThreadMember(resource.data.thread_id); - } + match /{path=**}/thread_messages/{docId} { + allow read: if isAuthorized() && isThreadMember(resource.data.thread_id); + } - match /space_threads/{threadId}/thread_messages/{docId} { - allow read: if isAuthorized() && isThreadMember(threadId); - allow delete: if isAuthorized() && (isSpaceAdmin(resource.data.space_id)); - allow update: if isAuthorized() && isThreadMember(threadId) && - request.resource.data.diff(resource.data).affectedKeys().hasOnly(["seen_by"]) && - request.resource.data.seen_by is list; - allow create: if isAuthorized() && isThreadMember(threadId) && request.resource.data.sender_id == request.auth.uid && - request.resource.data.keys().hasAll(["id", "thread_id", "sender_id", "message", "seen_by", "created_at"]) && - request.resource.data.id is string && - request.resource.data.thread_id is string && - request.resource.data.sender_id is string && - request.resource.data.message is string && - request.resource.data.seen_by is list && - request.resource.data.created_at is timestamp; - } + match /space_threads/{threadId}/thread_messages/{docId} { + allow read: if isAuthorized() && isThreadMember(threadId); + allow delete: if isAuthorized() && (isSpaceAdmin(resource.data.space_id)); + allow update: if isAuthorized() && isThreadMember(threadId) && + request.resource.data.diff(resource.data).affectedKeys().hasOnly(["seen_by"]) && + request.resource.data.seen_by is list; + allow create: if isAuthorized() && isThreadMember(threadId) && request.resource.data.sender_id == request.auth.uid && + request.resource.data.keys().hasAll(["id", "thread_id", "sender_id", "message", "seen_by", "created_at"]) && + request.resource.data.id is string && + request.resource.data.thread_id is string && + request.resource.data.sender_id is string && + request.resource.data.message is string && + request.resource.data.seen_by is list && + request.resource.data.created_at is timestamp; } -} \ No newline at end of file + } +} diff --git a/gradle.properties b/gradle.properties index 2cbd6d19..957f8ef9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,3 +21,4 @@ kotlin.code.style=official # resources declared in the library itself and none from the library's dependencies, # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true +android.enableR8.fullMode=false