Overview
Security is a critical aspect of NASAExplorer. This guide covers best practices for protecting sensitive credentials, implementing Firebase security rules, and maintaining a secure development workflow.
Sensitive Files Management
NASAExplorer keeps sensitive configuration files out of version control to prevent credential exposure.
Protected Files
Never commit these files to version control:
local.properties - Contains NASA API key
google-services.json - Contains Firebase configuration
- Any
.env files with credentials
keystore files for app signing
.gitignore Configuration
The project’s .gitignore file includes:
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
Notice that local.properties is listed twice for redundancy. This ensures it’s never accidentally committed.
API Key Security
NASA API Key Protection
The NASA API key is stored securely using BuildConfig:
1. Storage in local.properties
nasaApiKey=YOUR_NASA_API_KEY_HERE
2. Injection at Build Time
// Read local.properties
val localProperties = Properties().apply {
load(project.rootProject.file("local.properties").inputStream())
}
// Extract API key
val nasaApiKey: String = localProperties.getProperty("nasaApiKey") ?: ""
android {
defaultConfig {
// Inject as BuildConfig field (not visible in source code)
buildConfigField("String", "NASA_API_KEY", "\"$nasaApiKey\"")
}
buildFeatures {
buildConfig = true
}
}
3. Access via BuildConfig
class NasaRepository @Inject constructor(
private val nasaApiService: NasaApiService
) {
companion object {
private const val API_KEY = BuildConfig.NASA_API_KEY
}
suspend fun getImageOfTheDay(date: String? = null): NasaModel {
val response = nasaApiService.getImageOfTheDay(
apiKey = API_KEY,
date = date
)
return response.toNasaModel()
}
}
Why This Approach?
Build-Time Injection
The API key is injected during compilation, not stored in source code.
ProGuard Protection
In release builds, ProGuard obfuscates the BuildConfig class, making reverse engineering more difficult.
No Source Code Exposure
The key never appears in .kt files that get committed to Git.
Limitation: BuildConfig fields can still be extracted from APK files through reverse engineering. For production apps with strict security requirements, consider using a backend API proxy instead.
Firebase Security
Authentication Security
The AuthService implements secure authentication patterns:
class AuthService @Inject constructor(
private val firebaseAuth: FirebaseAuth
) {
suspend fun login(email: String, password: String): FirebaseUser? {
return firebaseAuth
.signInWithEmailAndPassword(email, password)
.await()
.user
}
suspend fun register(email: String, password: String): FirebaseUser? {
return firebaseAuth
.createUserWithEmailAndPassword(email, password)
.await()
.user
}
fun userLogout() {
firebaseAuth.signOut()
}
fun isUserLogged(): Boolean {
return getCurrentUser() != null
}
private fun getCurrentUser() = firebaseAuth.currentUser
}
Best Practices Implemented
- Coroutine-based async operations - No blocking calls
- Null safety - Proper handling of nullable
FirebaseUser
- Error propagation - Exceptions bubble up for proper handling
- Session management - Clean logout functionality
Firebase Realtime Database Rules
Development Rules (Current)
For development, the database uses test mode rules:
{
"rules": {
".read": true,
".write": true
}
}
Test mode allows unrestricted access. This is acceptable for development but must be changed before production deployment.
Production Rules (Recommended)
For production, implement user-specific access control:
{
"rules": {
"favorites": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid",
".validate": "newData.hasChildren(['id', 'title', 'url'])",
"$favoriteId": {
".validate": "newData.child('id').isString() && newData.child('title').isString() && newData.child('url').isString()"
}
}
},
"comments": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid",
"$favoriteId": {
".validate": "newData.isString() && newData.val().length <= 1000"
}
}
}
}
}
Rule Breakdown
Favorites Rules
"favorites": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid"
}
}
- User isolation: Each user can only access their own favorites
- Authentication required:
auth != null ensures user is logged in
- UID matching:
auth.uid == $uid verifies user owns the data
"comments": {
"$uid": {
"$favoriteId": {
".validate": "newData.isString() && newData.val().length <= 1000"
}
}
}
- Type validation: Ensures comments are strings
- Length limit: Prevents abuse with 1000 character max
- User-scoped: Comments tied to user’s favorites
Applying Security Rules
Open Realtime Database Rules
- Go to Realtime Database section
- Click on the Rules tab
Update Rules
Replace the existing rules with the production rules provided above.
Publish Changes
Click Publish to activate the new security rules.
Test Your App
Verify that:
- Users can only access their own favorites
- Unauthenticated requests are denied
- Data validation works correctly
After applying production rules, test thoroughly. Users will be unable to access data if authentication fails or rules are misconfigured.
Code-Level Security
User Authentication Checks
All database operations verify user authentication:
fun loadFavoriteImages() {
val userId = firebaseAuth.currentUser?.uid
if (userId != null) {
viewModelScope.launch {
try {
val favoriteRef = firebaseDatabase.reference
.child("favorites")
.child(userId)
val snapshot = favoriteRef.get().await()
// ... process data
} catch (e: Exception) {
_errorMessage.value = "Error al mostrar la lista de favoritos"
}
}
} else {
_errorMessage.value = "Usuario no autenticado"
}
}
Key Security Patterns
-
Null-safe UID extraction
val userId = firebaseAuth.currentUser?.uid
if (userId != null) { /* proceed */ }
-
User-scoped references
firebaseDatabase.reference.child("favorites").child(userId)
-
Error handling
try {
// Database operation
} catch (e: Exception) {
_errorMessage.value = "Error message"
}
-
Coroutine safety
viewModelScope.launch {
// Async operation with lifecycle awareness
}
Network Security
HTTPS Enforcement
All network communication uses HTTPS:
@Module
@InstallIn(SingletonComponent::class)
object ApiNetworkModule {
private const val BASE_URL = "https://api.nasa.gov/" // HTTPS
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
Firebase automatically uses HTTPS:
https://nasaexplorer-f002d-default-rtdb.europe-west1.firebasedatabase.app
Network Security Config (Optional)
For additional security, you can add a network security configuration:
res/xml/network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" />
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">api.nasa.gov</domain>
<domain includeSubdomains="true">firebase.google.com</domain>
</domain-config>
</network-security-config>
Reference in AndroidManifest.xml:
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
</application>
ProGuard/R8 Configuration
For release builds, enable code obfuscation:
android {
buildTypes {
release {
isMinifyEnabled = true // Enable code shrinking
isShrinkResources = true // Remove unused resources
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
ProGuard Rules for Firebase
# Firebase
-keepattributes Signature
-keepattributes *Annotation*
-keepattributes EnclosingMethod
-keepattributes InnerClasses
# Gson (used by Retrofit)
-keepattributes Signature
-keep class com.ccandeladev.nasaexplorer.data.api.** { *; }
-keep class com.ccandeladev.nasaexplorer.domain.** { *; }
# Firebase Realtime Database
-keepclassmembers class com.ccandeladev.nasaexplorer.domain.** {
*;
}
Security Checklist
Incident Response
If API Key is Compromised
Rotate Key Immediately
- Go to NASA API Portal
- Generate a new API key
- Update
local.properties with new key
- Invalidate the old key if possible
Review Access Logs
Check if the compromised key was abused (rate limit exceeded, unusual usage patterns).
Audit Repository
Search Git history to ensure the key was never committed:git log -p | grep "nasaApiKey"
If Firebase Config is Exposed
Assess Risk
Determine what was exposed (public keys vs. sensitive credentials).
Update Security Rules
Immediately apply strict production security rules if not already done.
Rotate Credentials
If sensitive credentials were exposed:
- Create a new Firebase project
- Migrate users and data
- Update app configuration
- Delete compromised project
Monitor Activity
Check Firebase Console for:
- Unusual authentication attempts
- Unexpected database reads/writes
- Abnormal traffic patterns
Additional Security Resources
Next Steps
With security configured:
- Review Firebase configuration
- Learn about testing strategies
- Review building and deployment
Security is an ongoing process. Regularly review and update your security measures as new vulnerabilities and best practices emerge.