Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement e2ee for locations & journeys #133

Merged
merged 38 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7d0d68c
WIP
cp-megh-l Dec 4, 2024
b3232a7
WIP
cp-megh-l Dec 6, 2024
78bd44a
WIP
cp-megh-l Dec 12, 2024
e9183dd
Revert "WIP"
cp-megh-l Dec 16, 2024
ab61d8c
WIP
cp-megh-l Dec 25, 2024
582e99e
WIP
cp-megh-l Dec 27, 2024
fc755ed
WIP - profile key can be removed
cp-megh-l Dec 27, 2024
db27d58
Merge branch 'refs/heads/main' into megh/implement-e2ee-for-locations-sp
cp-megh-l Dec 27, 2024
2192f62
WIP - lint fix
cp-megh-l Dec 27, 2024
6d2e61f
WIP - multidevice support
cp-megh-l Dec 31, 2024
7099ea9
finish - multidevice support
cp-megh-l Dec 31, 2024
2aed3d2
final commit
cp-megh-l Jan 1, 2025
9f4413c
live updates pending
cp-megh-l Jan 2, 2025
557f29e
final commit
cp-megh-l Jan 2, 2025
755fb1e
existing spaces changes
cp-megh-l Jan 2, 2025
1d1f1a7
fix lint
cp-megh-l Jan 2, 2025
e8eaecb
fix session error and existing users flow
cp-megh-l Jan 3, 2025
24bb9de
PR changes
cp-megh-l Jan 3, 2025
43e0540
PR changes
cp-megh-l Jan 3, 2025
d9901d6
PR changes
cp-megh-l Jan 6, 2025
9381c94
Merge branch 'refs/heads/main' into megh/implement-e2ee-for-locations-sp
cp-megh-l Jan 6, 2025
33bf832
merge main
cp-megh-l Jan 6, 2025
e7a451b
minor change
cp-megh-l Jan 6, 2025
65720cd
WIP
cp-megh-l Jan 7, 2025
cd86a5b
WIP
cp-megh-l Jan 7, 2025
69bbc7c
Merge branch 'refs/heads/main' into megh/implement-e2ee-for-locations-sp
cp-megh-l Jan 7, 2025
701c297
merge main
cp-megh-l Jan 7, 2025
d459d92
Merge branch 'refs/heads/megh/implement-e2ee-for-locations-sp' into m…
cp-megh-l Jan 7, 2025
a9102a9
final commit
cp-megh-l Jan 8, 2025
25db65d
PR changes
cp-megh-l Jan 9, 2025
bc3ad8e
PR changes
cp-megh-l Jan 9, 2025
8534114
Merge branch 'refs/heads/main' into megh/implement-e2ee-for-locations-sp
cp-megh-l Jan 9, 2025
659bfe2
minor change
cp-megh-l Jan 9, 2025
9c73d95
minor changes
cp-megh-l Jan 9, 2025
17c89bd
Add distributions rotation logic
cp-megh-l Jan 9, 2025
3b7092c
Merge remote-tracking branch 'origin/megh/implement-e2ee-for-location…
cp-megh-l Jan 9, 2025
53d85d3
Merge branch 'refs/heads/main' into megh/implement-e2ee-for-locations-sp
cp-megh-l Jan 9, 2025
fc6784b
minor changes
cp-megh-l Jan 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ android {
defaultConfig {
applicationId = "com.canopas.yourspace"
minSdk = 24
targetSdk = 34
versionCode = versionMajor * 1000000 + versionMinor * 10000 + versionBuild
versionName = "$versionMajor.$versionMinor.$versionBuild"
setProperty("archivesBaseName", "GroupTrack-$versionName-$versionCode")
Expand Down Expand Up @@ -103,12 +102,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 +207,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:1.1.6")

// 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
11 changes: 11 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,20 @@ class MainViewModel @Inject constructor(

init {
viewModelScope.launch {
val currentUser = authService.getUser()
val isExistingUser = currentUser != null
val showSetPinScreen =
isExistingUser && currentUser!!.identity_key_public?.toBytes()
.contentEquals(currentUser.identity_key_private?.toBytes())
val showEnterPinScreen =
isExistingUser && currentUser!!.identity_key_public?.toBytes()
.contentEquals(currentUser.identity_key_private?.toBytes()) && userPreferences.getPasskey()
.isNullOrEmpty()
val initialRoute = when {
!userPreferences.isIntroShown() -> AppDestinations.intro.path
userPreferences.currentUser == null -> AppDestinations.signIn.path
showSetPinScreen -> AppDestinations.setPin.path
showEnterPinScreen -> AppDestinations.enterPin.path
!userPreferences.isOnboardShown() -> AppDestinations.onboard.path
else -> AppDestinations.home.path
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,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) {
cp-megh-l marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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
@@ -0,0 +1,112 @@
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.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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()
val context = LocalContext.current
val invalidPinText by remember {
mutableStateOf(context.getString(R.string.enter_pin_error_text))
}

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
)
cp-megh-l marked this conversation as resolved.
Show resolved Hide resolved

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

Text(
cp-megh-l marked this conversation as resolved.
Show resolved Hide resolved
text = state.pinError ?: "",
color = if (!state.pinError.isNullOrEmpty()) MaterialTheme.colorScheme.error else Color.Transparent,
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(invalidPinText)
},
enabled = state.pin != "" && state.pinError == "",
modifier = Modifier.fillMaxWidth(),
showLoader = state.showLoader
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.canopas.yourspace.ui.flow.pin.enterpin

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.canopas.yourspace.data.service.auth.AuthService
import com.canopas.yourspace.data.storage.UserPreferences
import com.canopas.yourspace.data.utils.AppDispatcher
import com.canopas.yourspace.domain.utils.ConnectivityObserver
import com.canopas.yourspace.ui.navigation.AppDestinations
import com.canopas.yourspace.ui.navigation.AppNavigator
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class EnterPinViewModel @Inject constructor(
private val navigator: AppNavigator,
private val authService: AuthService,
private val appDispatcher: AppDispatcher,
private val userPreferences: UserPreferences,
private val connectivityObserver: ConnectivityObserver
) : ViewModel() {
private val _state = MutableStateFlow(EnterPinScreenState())
val state: StateFlow<EnterPinScreenState> = _state

init {
checkInternetConnection()
}

fun onPinChanged(newPin: String) {
_state.value = _state.value.copy(pin = newPin)
_state.value = _state.value.copy(pinError = if (newPin.length == 4) "" else null)
cp-megh-l marked this conversation as resolved.
Show resolved Hide resolved
}

fun checkInternetConnection() {
viewModelScope.launch(appDispatcher.IO) {
connectivityObserver.observe().collectLatest { status ->
_state.emit(
_state.value.copy(
connectivityStatus = status
)
)
}
}
}
cp-megh-l marked this conversation as resolved.
Show resolved Hide resolved

fun processPin(invalidPinText: String) = viewModelScope.launch(appDispatcher.MAIN) {
_state.value = _state.value.copy(showLoader = true)
val pin = state.value.pin
if (pin.length == 4) {
val isPinValid = authService.validatePasskey(passKey = pin)
if (isPinValid) {
userPreferences.setOnboardShown(true)
navigator.navigateTo(
AppDestinations.home.path,
popUpToRoute = AppDestinations.signIn.path,
inclusive = true
)
} else {
_state.value = _state.value.copy(pinError = invalidPinText)
cp-megh-l marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}

data class EnterPinScreenState(
val showLoader: Boolean = false,
val pin: String = "",
val pinError: String? = null,
val connectivityStatus: ConnectivityObserver.Status = ConnectivityObserver.Status.Available,
val error: Exception? = null
cp-megh-l marked this conversation as resolved.
Show resolved Hide resolved
)
Loading
Loading