Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
cp-megh-l committed Jan 21, 2025
1 parent d0d5f4b commit 2349625
Show file tree
Hide file tree
Showing 15 changed files with 98 additions and 83 deletions.
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ dependencies {
implementation("org.signal:libsignal-android:0.65.0")

// RevenueCat
implementation("com.revenuecat.purchases:purchases:7.0.0")
implementation("com.revenuecat.purchases:purchases:8.11.0")
implementation("com.revenuecat.purchases:purchases-ui:8.11.0")

implementation(project(":data"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ class YourSpaceApplication :

super<Application>.onCreate()
Timber.plant(Timber.DebugTree())
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = !BuildConfig.DEBUG
FirebaseCrashlytics.getInstance().isCrashlyticsCollectionEnabled = false
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
authService.addListener(this)
setNotificationChannel()

Purchases.logLevel = LogLevel.DEBUG
if (BuildConfig.DEBUG) {
Purchases.logLevel = LogLevel.DEBUG
}
Purchases.configure(PurchasesConfiguration.Builder(this, BuildConfig.REVENUECAT_API_KEY).build())

registerBatteryBroadcastReceiver()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.canopas.yourspace.data.models.user.ApiUserSession
import com.canopas.yourspace.data.repository.SpaceRepository
import com.canopas.yourspace.data.service.auth.AuthService
import com.canopas.yourspace.data.service.location.toBytes
import com.canopas.yourspace.data.service.user.ApiUserService
import com.canopas.yourspace.data.storage.UserPreferences
import com.canopas.yourspace.data.utils.AppDispatcher
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.canopas.yourspace.data.service.auth.AuthService
import com.canopas.yourspace.data.service.auth.FirebaseAuthService
import com.canopas.yourspace.data.service.location.toBytes
import com.canopas.yourspace.data.storage.UserPreferences
import com.canopas.yourspace.data.utils.AppDispatcher
import com.canopas.yourspace.domain.utils.ConnectivityObserver
Expand Down Expand Up @@ -42,6 +43,7 @@ class SignInMethodViewModel @Inject constructor(
_state.emit(_state.value.copy(showGoogleLoading = true))
try {
val firebaseToken = firebaseAuth.signInWithGoogleAuthCredential(account.idToken)
Timber.e("XXXXXX: Current User UID: ${firebaseAuth.currentUserUid}")
authService.verifiedGoogleLogin(
firebaseAuth.currentUserUid,
firebaseToken,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
Expand Down Expand Up @@ -86,6 +87,7 @@ class SetPinViewModel @Inject constructor(
_state.value = _state.value.copy(showLoader = false)
} catch (e: Exception) {
_state.value = _state.value.copy(error = e, showLoader = false)
Timber.e(e, "Failed to generate and save user keys")
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ android {
compileSdk = 35

defaultConfig {
minSdk = 23
minSdk = 24

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
javaCompileOptions {
Expand Down Expand Up @@ -92,6 +92,10 @@ dependencies {
// Place
implementation("com.google.android.libraries.places:places:4.1.0")

// RevenueCat
implementation("com.revenuecat.purchases:purchases:8.11.0")
implementation("com.revenuecat.purchases:purchases-ui:8.11.0")

// Signal Protocol
implementation("org.signal:libsignal-client:0.65.0")
implementation("org.signal:libsignal-android:0.65.0")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.canopas.yourspace.data.models.location

import androidx.annotation.Keep
import com.google.firebase.firestore.Blob
import com.squareup.moshi.JsonClass
import java.util.UUID

Expand All @@ -20,8 +19,8 @@ data class ApiLocation(
data class EncryptedApiLocation(
val id: String = UUID.randomUUID().toString(),
val user_id: String = "",
val latitude: Blob = Blob.fromBytes(ByteArray(0)), // Base64 encoded encrypted latitude
val longitude: Blob = Blob.fromBytes(ByteArray(0)), // Base64 encoded encrypted longitude
val latitude: String = "", // Base64 encoded encrypted latitude
val longitude: String = "", // Base64 encoded encrypted longitude
val created_at: Long = System.currentTimeMillis()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.canopas.yourspace.data.models.location
import android.location.Location
import androidx.annotation.Keep
import com.google.android.gms.maps.model.LatLng
import com.google.firebase.firestore.Blob
import com.squareup.moshi.JsonClass
import java.util.UUID

Expand All @@ -30,10 +29,10 @@ data class LocationJourney(
data class EncryptedLocationJourney(
val id: String = UUID.randomUUID().toString(),
val user_id: String = "",
val from_latitude: Blob = Blob.fromBytes(ByteArray(0)), // Encrypted latitude - from
val from_longitude: Blob = Blob.fromBytes(ByteArray(0)), // Encrypted longitude - from
val to_latitude: Blob? = null, // Encrypted latitude - to
val to_longitude: Blob? = null, // Encrypted longitude - to
val from_latitude: String = "", // Encrypted latitude - from
val from_longitude: String = "", // Encrypted longitude - from
val to_latitude: String? = null, // Encrypted latitude - to
val to_longitude: String? = null, // Encrypted longitude - to
val route_distance: Double? = null,
val route_duration: Long? = null,
val routes: List<EncryptedJourneyRoute> = emptyList(), // Encrypted journey routes
Expand All @@ -49,8 +48,8 @@ data class JourneyRoute(val latitude: Double = 0.0, val longitude: Double = 0.0)
@Keep
@JsonClass(generateAdapter = true)
data class EncryptedJourneyRoute(
val latitude: Blob = Blob.fromBytes(ByteArray(0)), // Encrypted latitude
val longitude: Blob = Blob.fromBytes(ByteArray(0)) // Encrypted longitude
val latitude: String = "", // Encrypted latitude
val longitude: String = "" // Encrypted longitude
)

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.canopas.yourspace.data.models.space

import androidx.annotation.Keep
import com.google.firebase.firestore.Blob
import com.google.firebase.firestore.Exclude
import java.util.UUID
import java.util.concurrent.TimeUnit
Expand All @@ -25,7 +24,7 @@ data class ApiSpaceMember(
val user_id: String = "",
val role: Int = SPACE_MEMBER_ROLE_MEMBER,
val location_enabled: Boolean = true,
val identity_key_public: Blob? = Blob.fromBytes(ByteArray(0)),
val identity_key_public: String? = null,
val created_at: Long? = System.currentTimeMillis()
)

Expand Down Expand Up @@ -77,24 +76,8 @@ data class MemberKeyData(
data class EncryptedDistribution(
val id: String = UUID.randomUUID().toString(),
val recipient_id: String = "",
val ephemeral_pub: Blob = Blob.fromBytes(ByteArray(0)), // 33 bytes (compressed distribution key)
val iv: Blob = Blob.fromBytes(ByteArray(0)), // 16 bytes
val ciphertext: Blob = Blob.fromBytes(ByteArray(0)), // AES/GCM ciphertext
val ephemeral_pub: String = "", // 33 bytes (compressed distribution key)
val iv: String = "", // 16 bytes
val ciphertext: String = "", // AES/GCM ciphertext
val created_at: Long = System.currentTimeMillis()
) {
init {
validateFieldSizes()
}

private fun validateFieldSizes() {
require(ephemeral_pub.toBytes().size == 33 || ephemeral_pub.toBytes().isEmpty()) {
"Invalid size for ephemeralPub: expected 33 bytes, got ${ephemeral_pub.toBytes().size} bytes."
}
require(iv.toBytes().size == 16 || iv.toBytes().isEmpty()) {
"Invalid size for iv: expected 16 bytes, got ${iv.toBytes().size} bytes."
}
require(ciphertext.toBytes().size <= 64 * 1024 || ciphertext.toBytes().isEmpty()) {
"Invalid size for ciphertext: maximum allowed size is 64 KB, got ${ciphertext.toBytes().size} bytes."
}
}
}
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.canopas.yourspace.data.models.user

import androidx.annotation.Keep
import com.google.firebase.firestore.Blob
import com.google.firebase.firestore.Exclude
import com.squareup.moshi.JsonClass
import java.util.UUID
Expand All @@ -27,12 +26,12 @@ data class ApiUser(
val fcm_token: String? = "",
val state: Int = USER_STATE_UNKNOWN,
val battery_pct: Float? = 0f,
val user_type: Int = FREE_USER,
val created_at: Long? = System.currentTimeMillis(),
val updated_at: Long? = System.currentTimeMillis(),
val identity_key_public: Blob? = Blob.fromBytes(ByteArray(0)),
val identity_key_private: Blob? = Blob.fromBytes(ByteArray(0)),
val identity_key_salt: Blob? = Blob.fromBytes(ByteArray(0)) // Salt for key derivation
val user_type: Int = PREMIUM_USER,
val identity_key_public: String? = null,
val identity_key_private: String? = null,
val identity_key_salt: String? = null // Salt for key derivation
) {
@get:Exclude
val fullName: String get() = "$first_name $last_name"
Expand All @@ -45,6 +44,12 @@ data class ApiUser(

@get:Exclude
val locationPermissionDenied: Boolean get() = state == USER_STATE_LOCATION_PERMISSION_DENIED

@get:Exclude
val isFreeUser: Boolean get() = user_type == FREE_USER

@get:Exclude
val isPremiumUser: Boolean get() = user_type == PREMIUM_USER
}

@Keep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import com.canopas.yourspace.data.utils.Config.FIRESTORE_COLLECTION_SPACE_MEMBER
import com.canopas.yourspace.data.utils.EphemeralECDHUtils
import com.canopas.yourspace.data.utils.PrivateKeyUtils
import com.canopas.yourspace.data.utils.snapshotFlow
import com.google.firebase.firestore.Blob
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.Query
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -129,8 +128,8 @@ class ApiLocationService @Inject constructor(

val location = EncryptedApiLocation(
user_id = userId,
latitude = Blob.fromBytes(encryptedLatitude.serialize()),
longitude = Blob.fromBytes(encryptedLongitude.serialize()),
latitude = encryptedLatitude.serialize().encodeToString(),
longitude = encryptedLongitude.serialize().encodeToString(),
created_at = recordedAt
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@ 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.google.firebase.firestore.Blob
import org.signal.libsignal.protocol.groups.GroupCipher
import timber.log.Timber
import java.util.Base64
import java.util.UUID

fun String.toBytes(): ByteArray {
return Base64.getDecoder().decode(this)
}

fun ByteArray.encodeToString(): String {
return Base64.getEncoder().encodeToString(this)
}

/**
* Convert an [EncryptedLocationJourney] to a [LocationJourney] using the provided [GroupCipher]
*/
fun EncryptedLocationJourney.toDecryptedLocationJourney(groupCipher: GroupCipher): LocationJourney? {
val decryptedFromLat = groupCipher.decrypt(from_latitude) ?: return null
val decryptedFromLong = groupCipher.decrypt(from_longitude) ?: return null
val decryptedToLat = to_latitude?.let { groupCipher.decrypt(it) }
val decryptedToLong = to_longitude?.let { groupCipher.decrypt(it) }
val decryptedFromLat = groupCipher.decryptPoint(from_latitude.toBytes()) ?: return null
val decryptedFromLong = groupCipher.decryptPoint(from_longitude.toBytes()) ?: return null
val decryptedToLat = to_latitude?.let { groupCipher.decryptPoint(it.toBytes()) }
val decryptedToLong = to_longitude?.let { groupCipher.decryptPoint(it.toBytes()) }

val decryptedRoutes = routes.map {
JourneyRoute(
latitude = groupCipher.decrypt(it.latitude) ?: return null,
longitude = groupCipher.decrypt(it.longitude) ?: return null
latitude = groupCipher.decryptPoint(it.latitude.toBytes()) ?: return null,
longitude = groupCipher.decryptPoint(it.longitude.toBytes()) ?: return null
)
}

Expand Down Expand Up @@ -49,15 +57,15 @@ fun LocationJourney.toEncryptedLocationJourney(
groupCipher: GroupCipher,
distributionId: UUID
): EncryptedLocationJourney? {
val encryptedFromLat = groupCipher.encrypt(distributionId, from_latitude) ?: return null
val encryptedFromLong = groupCipher.encrypt(distributionId, from_longitude) ?: return null
val encryptedToLat = to_latitude?.let { groupCipher.encrypt(distributionId, it) }
val encryptedToLong = to_longitude?.let { groupCipher.encrypt(distributionId, it) }
val encryptedFromLat = groupCipher.encryptPoint(distributionId, from_latitude) ?: return null
val encryptedFromLong = groupCipher.encryptPoint(distributionId, from_longitude) ?: return null
val encryptedToLat = to_latitude?.let { groupCipher.encryptPoint(distributionId, it) }
val encryptedToLong = to_longitude?.let { groupCipher.encryptPoint(distributionId, it) }

val encryptedRoutes = routes.map {
EncryptedJourneyRoute(
latitude = groupCipher.encrypt(distributionId, it.latitude) ?: return null,
longitude = groupCipher.encrypt(distributionId, it.longitude) ?: return null
latitude = groupCipher.encryptPoint(distributionId, it.latitude) ?: return null,
longitude = groupCipher.encryptPoint(distributionId, it.longitude) ?: return null
)
}

Expand All @@ -78,18 +86,18 @@ fun LocationJourney.toEncryptedLocationJourney(
)
}

fun GroupCipher.decrypt(data: Blob): Double? {
fun GroupCipher.decryptPoint(data: ByteArray): Double? {
return try {
decrypt(data.toBytes()).toString(Charsets.UTF_8).toDouble()
decrypt(data).encodeToString().toDouble()
} catch (e: Exception) {
Timber.e(e, "Failed to decrypt double")
null
}
}

fun GroupCipher.encrypt(distributionId: UUID, data: Double): Blob? {
fun GroupCipher.encryptPoint(distributionId: UUID, data: Double): String? {
return try {
Blob.fromBytes(encrypt(distributionId, data.toString().toByteArray(Charsets.UTF_8)).serialize())
(encrypt(distributionId, data.toString().toBytes()).serialize()).encodeToString()
} catch (e: Exception) {
Timber.e(e, "Failed to encrypt double")
null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.canopas.yourspace.data.models.user.ApiUser
import com.canopas.yourspace.data.models.user.FREE_USER
import com.canopas.yourspace.data.models.user.PREMIUM_USER
import com.canopas.yourspace.data.service.auth.AuthService
import com.canopas.yourspace.data.service.location.toBytes
import com.canopas.yourspace.data.service.place.ApiPlaceService
import com.canopas.yourspace.data.service.user.ApiUserService
import com.canopas.yourspace.data.storage.bufferedkeystore.BufferedSenderKeyStore
Expand All @@ -29,7 +30,6 @@ import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.jvm.Throws

@Singleton
class ApiSpaceService @Inject constructor(
Expand Down Expand Up @@ -78,7 +78,6 @@ class ApiSpaceService @Inject constructor(
return spaceMemberRef(spaceId).get().await().toObjects(ApiSpaceMember::class.java)
}

@Throws(IllegalStateException::class)
suspend fun joinSpace(spaceId: String, role: Int = SPACE_MEMBER_ROLE_MEMBER, enableEncryption: Boolean? = null) {
val user = authService.currentUser ?: throw IllegalStateException("No authenticated user")
val isEncryptionEnabled = enableEncryption ?: kotlin.run {
Expand All @@ -88,7 +87,7 @@ class ApiSpaceService @Inject constructor(
when {
isEncryptionEnabled && user.user_type == FREE_USER -> {
// Redirect to subscription page
throw IllegalStateException("User must be a premium user to join an encrypted space")
throw SubscriptionRequiredException("User must be a premium user to join an encrypted space")
}
!isEncryptionEnabled && user.user_type == PREMIUM_USER -> {
// Notify/Alert the user that they are joining an unencrypted space
Expand Down Expand Up @@ -261,3 +260,8 @@ class ApiSpaceService @Inject constructor(
}
}
}

/**
* Exception thrown when a user tries to perform an action that requires a subscription.
*/
class SubscriptionRequiredException(message: String) : Exception(message)
Loading

0 comments on commit 2349625

Please sign in to comment.