diff --git a/app/src/main/java/com/astralquarks/notes/auth/AuthManager.kt b/app/src/main/java/com/astralquarks/notes/auth/AuthManager.kt index 0e849c6..5f0d0c7 100644 --- a/app/src/main/java/com/astralquarks/notes/auth/AuthManager.kt +++ b/app/src/main/java/com/astralquarks/notes/auth/AuthManager.kt @@ -39,6 +39,8 @@ enum class SyncStatus { class AuthManager(private val context: Context) { + lateinit var vaultSecurityManager: com.astralquarks.notes.security.VaultSecurityManager + private val auth: FirebaseAuth = FirebaseAuth.getInstance() private val firestore: FirebaseFirestore = FirebaseFirestore.getInstance() private val credentialManager = CredentialManager.create(context) @@ -79,21 +81,13 @@ class AuthManager(private val context: Context) { get() = _currentUser.value?.photoUrl?.toString() /** - * Derives or retrieves the 256-bit AES master encryption key for this user. - * Compatible with Web Crypto API standards using PBKDF2WithHmacSHA256. + * Retrieves the 256-bit AES master encryption key for this user from VaultSecurityManager. */ fun getEncryptionKey(): SecretKey? { - val uid = userId ?: return null - val storedPass = vaultPrefs.getString("vault_key_$uid", null) ?: uid - val saltStr = vaultPrefs.getString("vault_salt_$uid", null) - val salt = if (!saltStr.isNullOrBlank()) { - Base64.decode(saltStr, Base64.NO_WRAP) - } else { - val newSalt = CryptoEngine.generateSalt() - vaultPrefs.edit().putString("vault_salt_$uid", Base64.encodeToString(newSalt, Base64.NO_WRAP)).apply() - newSalt + if (::vaultSecurityManager.isInitialized) { + return vaultSecurityManager.getVaultKey() } - return CryptoEngine.deriveKey(storedPass, salt) + return null } private fun getEffectiveWebClientId(): String { @@ -294,7 +288,7 @@ class AuthManager(private val context: Context) { * Observes real-time note updates from new V2 Firestore path: user/{uid}/notes */ @OptIn(ExperimentalCoroutinesApi::class) - fun observeFirestoreNotes(): Flow> = currentUser.flatMapLatest { user -> + fun observeFirestoreNotes(): Flow> = kotlinx.coroutines.flow.combine(currentUser, if (::vaultSecurityManager.isInitialized) vaultSecurityManager.isVaultUnlocked else flowOf(false)) { user, _ -> user }.flatMapLatest { user -> val uid = user?.uid if (uid == null) { flowOf(emptyList()) diff --git a/app/src/main/java/com/astralquarks/notes/model/Note.kt b/app/src/main/java/com/astralquarks/notes/model/Note.kt index 6a1e149..72b550d 100644 --- a/app/src/main/java/com/astralquarks/notes/model/Note.kt +++ b/app/src/main/java/com/astralquarks/notes/model/Note.kt @@ -27,6 +27,9 @@ data class Note( val deviceId: String = "", val isDeleted: Boolean = false ) { + @androidx.room.Ignore + var isDecryptionFailed: Boolean = false + /** * Serializes note to V2 Firestore schema. * When secretKey is provided, title, content, tags, and imageUrls are encrypted. @@ -100,21 +103,30 @@ data class Note( var tags = (map["tags"] as? List<*>)?.filterIsInstance() ?: emptyList() var imageUrls = (map["imageUrls"] as? List<*>)?.filterIsInstance() ?: emptyList() - if (isEncrypted && !encryptedData.isNullOrBlank() && !iv.isNullOrBlank() && secretKey != null) { - try { - val decrypted = CryptoEngine.decryptNotePayload(encryptedData, iv, secretKey) - title = decrypted.title - content = decrypted.content - tags = decrypted.tags - imageUrls = decrypted.imageUrls - } catch (e: Exception) { - // If decryption key does not match, retain placeholder without crashing + var isDecryptionFailed = false + + if (isEncrypted && !encryptedData.isNullOrBlank() && !iv.isNullOrBlank()) { + if (secretKey != null) { + try { + val decrypted = CryptoEngine.decryptNotePayload(encryptedData, iv, secretKey) + title = decrypted.title + content = decrypted.content + tags = decrypted.tags + imageUrls = decrypted.imageUrls + } catch (e: Exception) { + // If decryption key does not match, retain placeholder without crashing + isDecryptionFailed = true + title = "[Encrypted Note]" + content = "Unable to decrypt content with current vault key." + } + } else { + isDecryptionFailed = true title = "[Encrypted Note]" content = "Unable to decrypt content with current vault key." } } - return Note( + val note = Note( id = id, title = title, content = content, @@ -133,6 +145,8 @@ data class Note( deviceId = deviceId, isDeleted = isDeleted ) + note.isDecryptionFailed = isDecryptionFailed + return note } fun fromFirestoreMap(map: Map): Note = fromFirestoreV2Map(map, null) diff --git a/app/src/main/java/com/astralquarks/notes/viewmodel/NotesViewModel.kt b/app/src/main/java/com/astralquarks/notes/viewmodel/NotesViewModel.kt index 94f2cfa..8933b81 100644 --- a/app/src/main/java/com/astralquarks/notes/viewmodel/NotesViewModel.kt +++ b/app/src/main/java/com/astralquarks/notes/viewmodel/NotesViewModel.kt @@ -63,6 +63,8 @@ class NotesViewModel(private val application: Application) : AndroidViewModel(ap val currentDestination: StateFlow = _currentDestination.asStateFlow() init { + authManager.vaultSecurityManager = vaultSecurityManager + // Observe user sign-in state to automatically trigger full bidirectional sync viewModelScope.launch { authManager.currentUser.collect { user ->