Skip to content

Commit

Permalink
Implement E2EE for locations & journeys
Browse files Browse the repository at this point in the history
Implement E2EE for locations & journeys
  • Loading branch information
cp-megh-l authored Jan 9, 2025
2 parents 87cf70d + fc6784b commit 53dadff
Show file tree
Hide file tree
Showing 45 changed files with 2,502 additions and 490 deletions.
18 changes: 12 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import org.jetbrains.kotlin.konan.properties.hasProperty
import java.util.Properties

Expand All @@ -14,6 +15,7 @@ plugins {
var versionMajor = 1
var versionMinor = 0
var versionBuild = 0
val targetSdkVersion: Int = 34

android {
namespace = "com.canopas.yourspace"
Expand All @@ -30,7 +32,8 @@ android {
defaultConfig {
applicationId = "com.canopas.yourspace"
minSdk = 24
targetSdk = 34
targetSdk = targetSdkVersion

versionCode = versionMajor * 1000000 + versionMinor * 10000 + versionBuild
versionName = "$versionMajor.$versionMinor.$versionBuild"
setProperty("archivesBaseName", "GroupTrack-$versionName-$versionCode")
Expand Down Expand Up @@ -60,7 +63,6 @@ android {
buildConfigField("String", "PLACE_API_KEY", "\"${p.getProperty("PLACE_API_KEY")}\"")
}
}

signingConfigs {
if (System.getenv("APKSIGN_KEYSTORE") != null) {
create("release") {
Expand Down Expand Up @@ -103,12 +105,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 Expand Up @@ -208,10 +210,14 @@ dependencies {
implementation("androidx.core:core-splashscreen:1.0.1")

// Desugaring
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.3")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")

// Gson
implementation("com.google.code.gson:gson:2.10.1")

// Signal Protocol
implementation("org.signal:libsignal-client:0.64.1")
implementation("org.signal:libsignal-android:0.64.1")

implementation(project(":data"))
}
8 changes: 8 additions & 0 deletions app/src/main/java/com/canopas/yourspace/ui/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import com.canopas.yourspace.ui.flow.messages.chat.MessagesScreen
import com.canopas.yourspace.ui.flow.messages.thread.ThreadsScreen
import com.canopas.yourspace.ui.flow.onboard.OnboardScreen
import com.canopas.yourspace.ui.flow.permission.EnablePermissionsScreen
import com.canopas.yourspace.ui.flow.pin.enterpin.EnterPinScreen
import com.canopas.yourspace.ui.flow.pin.setpin.SetPinScreen
import com.canopas.yourspace.ui.flow.settings.SettingsScreen
import com.canopas.yourspace.ui.flow.settings.profile.EditProfileScreen
import com.canopas.yourspace.ui.flow.settings.space.SpaceProfileScreen
Expand Down Expand Up @@ -124,6 +126,12 @@ fun MainApp(viewModel: MainViewModel) {
slideComposable(AppDestinations.signIn.path) {
SignInMethodsScreen()
}
slideComposable(AppDestinations.setPin.path) {
SetPinScreen()
}
slideComposable(AppDestinations.enterPin.path) {
EnterPinScreen()
}

slideComposable(AppDestinations.home.path) {
navController.currentBackStackEntry
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/com/canopas/yourspace/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,18 @@ class MainViewModel @Inject constructor(

init {
viewModelScope.launch {
val currentUser = authService.getUser()
val isExistingUser = currentUser != null
val identityKeysMatch = currentUser?.let {
it.identity_key_public?.toBytes().contentEquals(it.identity_key_private?.toBytes())
} ?: false
val showSetPinScreen = isExistingUser && identityKeysMatch
val showEnterPinScreen = showSetPinScreen && userPreferences.getPasskey().isNullOrEmpty()
val initialRoute = when {
!userPreferences.isIntroShown() -> AppDestinations.intro.path
userPreferences.currentUser == null -> AppDestinations.signIn.path
showEnterPinScreen -> AppDestinations.enterPin.path
showSetPinScreen -> AppDestinations.setPin.path
!userPreferences.isOnboardShown() -> AppDestinations.onboard.path
else -> AppDestinations.home.path
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ fun OtpInputField(
pinText: String,
onPinTextChange: (String) -> Unit,
textStyle: TextStyle = AppTheme.appTypography.header2,
digitCount: Int = 6
digitCount: Int = 6,
keyboardType: KeyboardType = KeyboardType.Text
) {
val focusRequester = remember { FocusRequester() }
BoxWithConstraints(
Expand All @@ -55,7 +56,7 @@ fun OtpInputField(
onPinTextChange(it)
}
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
keyboardOptions = KeyboardOptions(keyboardType = keyboardType),
modifier = Modifier.focusRequester(focusRequester),
decorationBox = {
Row(
Expand All @@ -67,7 +68,7 @@ fun OtpInputField(
repeat(digitCount) { index ->
OTPDigit(index, pinText, textStyle, focusRequester, width = width)

if (index == 2) {
if (index == 2 && digitCount > 4) {
HorizontalDivider(
modifier = Modifier
.width(16.dp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ class SignInMethodViewModel @Inject constructor(
_state.emit(_state.value.copy(showGoogleLoading = true))
try {
val firebaseToken = firebaseAuth.signInWithGoogleAuthCredential(account.idToken)
val isNewUser = authService.verifiedGoogleLogin(
authService.verifiedGoogleLogin(
firebaseAuth.currentUserUid,
firebaseToken,
account
)
onSignUp(isNewUser)
onSignUp()
_state.emit(_state.value.copy(showGoogleLoading = false))
} catch (e: Exception) {
Timber.e(e, "Failed to sign in with google")
Expand All @@ -65,7 +65,7 @@ class SignInMethodViewModel @Inject constructor(
_state.emit(_state.value.copy(showAppleLoading = true))
try {
val firebaseToken = authResult.user?.getIdToken(true)?.await()
val isNewUser = authService.verifiedAppleLogin(
authService.verifiedAppleLogin(
firebaseAuth.currentUserUid,
firebaseToken?.token ?: "",
authResult.user ?: run {
Expand All @@ -78,7 +78,7 @@ class SignInMethodViewModel @Inject constructor(
return@launch
}
)
onSignUp(isNewUser)
onSignUp()
_state.emit(_state.value.copy(showAppleLoading = false))
} catch (e: Exception) {
Timber.e(e, "Failed to sign in with Apple")
Expand All @@ -95,17 +95,22 @@ class SignInMethodViewModel @Inject constructor(
_state.value = _state.value.copy(error = null)
}

private fun onSignUp(isNewUser: Boolean) = viewModelScope.launch(appDispatcher.MAIN) {
if (isNewUser) {
private fun onSignUp() = viewModelScope.launch(appDispatcher.MAIN) {
val currentUser = authService.currentUser ?: return@launch
val showSetPinScreen = currentUser.identity_key_public?.toBytes()
.contentEquals(currentUser.identity_key_private?.toBytes())
val showEnterPinScreen = !showSetPinScreen && userPreferences.getPasskey()
.isNullOrEmpty()

if (showSetPinScreen) {
navigator.navigateTo(
AppDestinations.onboard.path,
AppDestinations.setPin.path,
popUpToRoute = AppDestinations.signIn.path,
inclusive = true
)
} else {
userPreferences.setOnboardShown(true)
} else if (showEnterPinScreen) {
navigator.navigateTo(
AppDestinations.home.path,
AppDestinations.enterPin.path,
popUpToRoute = AppDestinations.signIn.path,
inclusive = true
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ private fun MemberInfoView(
val state by viewModel.state.collectAsState()

var address by remember { mutableStateOf("") }
val time = timeAgo(location?.created_at ?: 0)
val time = location?.created_at?.let { timeAgo(it) } ?: ""
val userStateText = if (user.noNetwork) {
stringResource(R.string.map_selected_user_item_no_network_state)
} else if (user.locationPermissionDenied) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ fun JourneyLocationItem(
.padding(start = 16.dp)
.weight(1f)
) {
val time = getFormattedJourneyTime(location.created_at ?: 0, location.update_at ?: 0)
val time = getFormattedJourneyTime(location.created_at ?: 0, location.updated_at ?: 0)
val distance = getDistanceString(location.route_distance ?: 0.0)

PlaceInfo(title, "$time - $distance")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ private fun JourneyInfo(journey: LocationJourney) {
.padding(start = 16.dp)
.weight(1f)
) {
journey.update_at?.let { getFormattedLocationTime(it) }
journey.updated_at?.let { getFormattedLocationTime(it) }
?.let { PlaceInfo(toAddressStr, it) }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,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)
if (journey == null) {
_state.value = _state.value.copy(
isLoading = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class JourneyTimelineViewModel @Inject constructor(
try {
val from = _state.value.selectedTimeFrom
val to = _state.value.selectedTimeTo
val lastJourneyTime = allJourneys.minOfOrNull { it.update_at!! }
val lastJourneyTime = allJourneys.minOfOrNull { it.updated_at }

val locations = if (loadMore) {
journeyService.getMoreJourneyHistory(userId, lastJourneyTime)
Expand All @@ -105,8 +105,8 @@ class JourneyTimelineViewModel @Inject constructor(
}

val filteredLocations = locations.filter {
(it.created_at?.let { created -> created in from..to } ?: false) ||
(it.update_at?.let { updated -> updated in from..to } ?: false)
it.created_at in from..to ||
it.updated_at in from..to
}

val locationJourneys = (allJourneys + filteredLocations).groupByDate()
Expand Down Expand Up @@ -158,12 +158,12 @@ class JourneyTimelineViewModel @Inject constructor(

private fun List<LocationJourney>.groupByDate(): Map<Long, List<LocationJourney>> {
val journeys = this.distinctBy { it.id }
.sortedByDescending { it.update_at!! }
.sortedByDescending { it.updated_at }

val groupedItems = mutableMapOf<Long, MutableList<LocationJourney>>()

for (journey in journeys) {
val date = getDayStartTimestamp(journey.created_at!!)
val date = getDayStartTimestamp(journey.created_at)

if (!groupedItems.containsKey(date)) {
groupedItems[date] = mutableListOf()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.canopas.yourspace.ui.flow.pin.enterpin

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.canopas.yourspace.R
import com.canopas.yourspace.ui.component.OtpInputField
import com.canopas.yourspace.ui.component.PrimaryButton

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EnterPinScreen() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.enter_pin_top_bar_title)) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.background
)
)
},
containerColor = MaterialTheme.colorScheme.background
) {
EnterPinContent(modifier = Modifier.padding(it))
}
}

@Composable
private fun EnterPinContent(modifier: Modifier) {
val viewModel = hiltViewModel<EnterPinViewModel>()
val state by viewModel.state.collectAsState()

Column(
modifier = modifier
.padding(32.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stringResource(R.string.enter_pin_header_text_part_one),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
textAlign = TextAlign.Center
)

Text(
text = stringResource(R.string.enter_pin_header_text_part_two),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
textAlign = TextAlign.Center
)

OtpInputField(
pinText = state.pin,
onPinTextChange = { viewModel.onPinChanged(it) },
digitCount = 4,
keyboardType = KeyboardType.Number
)

Spacer(modifier = Modifier.height(16.dp))

if (state.isPinInvalid) {
Text(
text = stringResource(R.string.enter_pin_invalid_pin_text),
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 8.dp)
)
}

Spacer(modifier = Modifier.height(24.dp))

PrimaryButton(
label = stringResource(R.string.enter_pin_continue_button_text),
onClick = {
viewModel.processPin()
},
enabled = state.pin != "" && !state.isPinInvalid && state.pin.length == 4,
modifier = Modifier.fillMaxWidth(),
showLoader = state.showLoader
)
}
}
Loading

0 comments on commit 53dadff

Please sign in to comment.