diff --git a/CERTIFICATE_SECURITY.md b/CERTIFICATE_SECURITY.md
new file mode 100644
index 0000000..836cdf9
--- /dev/null
+++ b/CERTIFICATE_SECURITY.md
@@ -0,0 +1,184 @@
+# Certificate Security Configuration
+
+This document explains the certificate validation configuration for development and production builds.
+
+## Overview
+
+The notification system uses different certificate validation strategies depending on the build type:
+
+- **Debug builds**: Allow self-signed certificates for development
+- **Release builds**: Strict certificate validation using system CAs only
+
+## Build-Specific Configuration
+
+### Debug Builds (Development)
+
+**File**: `demo-app/app/src/debug/res/xml/network_security_config.xml`
+
+- Allows self-signed certificates for localhost, 127.0.0.1, and 10.0.2.2 (Android emulator)
+- Trusts both system CAs and user-added certificates
+- Includes debug overrides for development flexibility
+- **WARNING**: Never use debug configuration in production
+
+### Release Builds (Production)
+
+**File**: `demo-app/app/src/release/res/xml/network_security_config.xml`
+
+- Strict certificate validation using system CAs only
+- No user certificate trust for maximum security
+- Requires proper CA-signed certificates
+- Production domain placeholder needs to be updated
+
+## Android App Implementation
+
+### HTTP Client Creation
+
+The `MainActivity.createHttpClient()` method automatically selects the appropriate client:
+
+```kotlin
+private fun createHttpClient(): OkHttpClient {
+ return if (BuildConfig.DEBUG) {
+ createDebugHttpClient() // Allows self-signed certs
+ } else {
+ createReleaseHttpClient() // Strict validation
+ }
+}
+```
+
+### Debug Client Features
+
+- Logs certificate acceptance for debugging
+- Accepts self-signed certificates
+- Flexible hostname verification
+- Development-friendly error handling
+
+### Release Client Features
+
+- Uses system certificate authorities only
+- Respects network security configuration
+- Strict hostname verification
+- Production-grade security
+
+## Production Deployment Requirements
+
+### 1. Backend Certificate Setup
+
+For production deployment, replace the self-signed certificate in `app-backend/` with proper CA-signed certificates:
+
+```bash
+# Replace these files with CA-signed certificates:
+app-backend/cert.pem # Public certificate
+app-backend/key.pem # Private key
+```
+
+### 2. Certificate Options
+
+**Option A: Let's Encrypt (Free)**
+```bash
+certbot certonly --standalone -d your-domain.com
+cp /etc/letsencrypt/live/your-domain.com/fullchain.pem app-backend/cert.pem
+cp /etc/letsencrypt/live/your-domain.com/privkey.pem app-backend/key.pem
+```
+
+**Option B: Commercial CA**
+- Purchase certificate from trusted CA (DigiCert, Comodo, etc.)
+- Follow CA's installation instructions
+- Place certificate files in `app-backend/`
+
+**Option C: Internal CA (Enterprise)**
+- Use organizational certificate authority
+- Ensure certificates are trusted by target devices
+- Consider certificate pinning for additional security
+
+### 3. Update Production Domain
+
+Edit `demo-app/app/src/release/res/xml/network_security_config.xml`:
+
+```xml
+your-actual-domain.com
+```
+
+### 4. Android App Configuration
+
+Update the default backend URL in `SettingsActivity` to use the production domain:
+
+```kotlin
+const val DEFAULT_BACKEND_URL = "https://your-production-domain.com:8443"
+```
+
+## Security Verification
+
+### Testing Debug Build
+
+1. Build debug APK: `./gradlew assembleDebug`
+2. Install on test device
+3. Verify self-signed certificate acceptance
+4. Check logs for certificate debugging messages
+
+### Testing Release Build
+
+1. Build release APK: `./gradlew assembleRelease`
+2. Install on test device
+3. Verify rejection of self-signed certificates
+4. Confirm only CA-signed certificates are accepted
+
+### Certificate Validation Test
+
+```bash
+# Test certificate chain
+openssl s_client -connect your-domain.com:8443 -showcerts
+
+# Verify certificate expiration
+openssl x509 -in app-backend/cert.pem -text -noout | grep "Not After"
+```
+
+## Security Best Practices
+
+1. **Never deploy debug builds to production**
+2. **Use HTTPS everywhere** - no HTTP in production
+3. **Monitor certificate expiration** - set up renewal alerts
+4. **Consider certificate pinning** for high-security applications
+5. **Regular security audits** of certificate configuration
+6. **Backup certificate private keys** securely
+
+## Troubleshooting
+
+### Common Issues
+
+**Certificate not trusted in release build**
+- Verify certificate is signed by trusted CA
+- Check network_security_config.xml domain configuration
+- Ensure certificate matches domain name
+
+**Debug build not accepting self-signed certificate**
+- Verify debug network_security_config.xml exists
+- Check build type configuration
+- Review OkHttp client implementation
+
+**Production deployment fails**
+- Verify certificate and private key match
+- Check certificate expiration date
+- Ensure proper file permissions on server
+
+### Logging
+
+The app provides detailed logging for certificate validation:
+
+- Debug builds: Certificate acceptance logged
+- Release builds: Strict validation logged
+- Network errors: Detailed error messages
+
+## Monitoring
+
+For production deployments, monitor:
+
+- Certificate expiration dates
+- SSL/TLS handshake failures
+- Certificate validation errors
+- Client connection success rates
+
+---
+
+**Last Updated**: January 2025
+**Security Level**: Production Ready
+**Next Review**: Certificate expiration check
diff --git a/app-backend/go.mod b/app-backend/go.mod
index f1a7587..1bcad31 100644
--- a/app-backend/go.mod
+++ b/app-backend/go.mod
@@ -1,3 +1,3 @@
module app-backend
-go 1.24.3
+go 1.24.5
diff --git a/app-backend/main.go b/app-backend/main.go
index bbee3d7..8b00516 100644
--- a/app-backend/main.go
+++ b/app-backend/main.go
@@ -12,6 +12,7 @@ import (
"log"
"net/http"
"os"
+ "strings"
"sync"
"time"
)
@@ -77,11 +78,111 @@ func (ts *TokenStore) Count() int {
return len(ts.tokenIDs)
}
+// RequestLog represents a structured log entry for HTTP requests
+type RequestLog struct {
+ Timestamp time.Time `json:"timestamp"`
+ Method string `json:"method"`
+ Path string `json:"path"`
+ RemoteAddr string `json:"remote_addr"`
+ UserAgent string `json:"user_agent"`
+ StatusCode int `json:"status_code"`
+ ResponseTime int64 `json:"response_time_ms"`
+ BodySize int64 `json:"body_size"`
+ Error string `json:"error,omitempty"`
+}
+
+// ResponseWriter wrapper to capture status code and response size
+type loggingResponseWriter struct {
+ http.ResponseWriter
+ statusCode int
+ bodySize int64
+}
+
+func (lrw *loggingResponseWriter) WriteHeader(code int) {
+ lrw.statusCode = code
+ lrw.ResponseWriter.WriteHeader(code)
+}
+
+func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
+ size, err := lrw.ResponseWriter.Write(b)
+ lrw.bodySize += int64(size)
+ return size, err
+}
+
var (
tokenStore = NewTokenStore()
publicKeyHash string
)
+// loggingMiddleware wraps HTTP handlers to provide structured logging
+func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+
+ // Create logging response writer
+ lrw := &loggingResponseWriter{
+ ResponseWriter: w,
+ statusCode: 200, // Default status code
+ }
+
+ // Call the next handler
+ next(lrw, r)
+
+ // Calculate response time
+ responseTime := time.Since(start).Milliseconds()
+
+ // Create structured log entry
+ logEntry := RequestLog{
+ Timestamp: start,
+ Method: r.Method,
+ Path: r.URL.Path,
+ RemoteAddr: getClientIP(r),
+ UserAgent: r.UserAgent(),
+ StatusCode: lrw.statusCode,
+ ResponseTime: responseTime,
+ BodySize: lrw.bodySize,
+ }
+
+ // Add error field for non-2xx responses
+ if lrw.statusCode >= 400 {
+ logEntry.Error = http.StatusText(lrw.statusCode)
+ }
+
+ // Log as JSON
+ logJSON, err := json.Marshal(logEntry)
+ if err != nil {
+ log.Printf("Error marshaling log entry: %v", err)
+ return
+ }
+
+ log.Printf("REQUEST_LOG: %s", string(logJSON))
+ }
+}
+
+// getClientIP extracts the real client IP from request headers
+func getClientIP(r *http.Request) string {
+ // Check X-Forwarded-For header (for proxies/load balancers)
+ if xForwardedFor := r.Header.Get("X-Forwarded-For"); xForwardedFor != "" {
+ // X-Forwarded-For can contain multiple IPs, take the first one
+ ifs := strings.Split(xForwardedFor, ",")
+ if len(ifs) > 0 {
+ return strings.TrimSpace(ifs[0])
+ }
+ }
+
+ // Check X-Real-IP header (for nginx)
+ if xRealIP := r.Header.Get("X-Real-IP"); xRealIP != "" {
+ return xRealIP
+ }
+
+ // Fall back to RemoteAddr
+ // Remove port if present
+ if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
+ return r.RemoteAddr[:idx]
+ }
+ return r.RemoteAddr
+}
+
func main() {
flag.Parse()
@@ -101,9 +202,9 @@ func main() {
publicKeyHash = computePublicKeyHash(publicKeyPEM)
log.Printf("Public key hash computed: %s", publicKeyHash[:16]+"...")
- http.HandleFunc("/register", handleRegister)
- http.HandleFunc("/send-all", handleSendAll)
- http.HandleFunc("/", handleHome)
+ http.HandleFunc("/register", loggingMiddleware(handleRegister))
+ http.HandleFunc("/send-all", loggingMiddleware(handleSendAll))
+ http.HandleFunc("/", loggingMiddleware(handleHome))
log.Printf("App Backend Server starting on HTTPS port %s", *port)
log.Printf("Web interface available at: https://localhost:%s", *port)
diff --git a/demo-app/app/build.gradle b/demo-app/app/build.gradle
index f1ae51c..6967e27 100644
--- a/demo-app/app/build.gradle
+++ b/demo-app/app/build.gradle
@@ -26,11 +26,11 @@ android {
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_11
+ targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
- jvmTarget = '1.8'
+ jvmTarget = '11'
}
}
diff --git a/demo-app/app/src/main/res/xml/network_security_config.xml b/demo-app/app/src/debug/res/xml/network_security_config.xml
similarity index 53%
rename from demo-app/app/src/main/res/xml/network_security_config.xml
rename to demo-app/app/src/debug/res/xml/network_security_config.xml
index 6573027..6d8ae04 100644
--- a/demo-app/app/src/main/res/xml/network_security_config.xml
+++ b/demo-app/app/src/debug/res/xml/network_security_config.xml
@@ -1,9 +1,12 @@
+
10.0.2.2
+ localhost
+ 127.0.0.1
-
+
@@ -17,4 +20,12 @@
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo-app/app/src/main/java/org/nella/rn/demo/MainActivity.kt b/demo-app/app/src/main/java/org/nella/rn/demo/MainActivity.kt
index 8a24127..aac13a0 100644
--- a/demo-app/app/src/main/java/org/nella/rn/demo/MainActivity.kt
+++ b/demo-app/app/src/main/java/org/nella/rn/demo/MainActivity.kt
@@ -33,10 +33,72 @@ class MainActivity : AppCompatActivity() {
private lateinit var registerButton: Button
private lateinit var statusText: TextView
- private val client = createUnsafeOkHttpClient()
+ private val client = createHttpClient()
companion object {
private const val TAG = "MainActivity"
+ // Set to true to force debug certificate behavior for testing
+ // WARNING: Never set to true in production builds
+ private const val FORCE_DEBUG_CERTIFICATES = false
+ }
+
+ /**
+ * Securely wipes a string from memory by overwriting its backing array
+ * Returns empty string to replace the original variable
+ */
+ private fun secureWipeString(value: String?): String {
+ if (value == null) return ""
+
+ try {
+ // Convert to char array and overwrite with zeros
+ val chars = value.toCharArray()
+ for (i in chars.indices) {
+ chars[i] = '\u0000'
+ }
+
+ // Also try to overwrite the byte representation
+ val bytes = value.toByteArray()
+ for (i in bytes.indices) {
+ bytes[i] = 0
+ }
+
+ Log.d(TAG, "Securely wiped string from memory (${value.length} chars)")
+ } catch (e: Exception) {
+ Log.w(TAG, "Warning: Could not securely wipe string from memory", e)
+ }
+
+ return "" // Return empty string to replace original variable
+ }
+
+ /**
+ * Securely wipes a byte array from memory
+ */
+ private fun secureWipeBytes(bytes: ByteArray?) {
+ if (bytes == null) return
+
+ try {
+ for (i in bytes.indices) {
+ bytes[i] = 0
+ }
+ Log.d(TAG, "Securely wiped byte array from memory (${bytes.size} bytes)")
+ } catch (e: Exception) {
+ Log.w(TAG, "Warning: Could not securely wipe byte array from memory", e)
+ }
+ }
+
+ /**
+ * Securely wipes a SecretKey from memory
+ */
+ private fun secureWipeSecretKey(key: SecretKey?) {
+ if (key == null) return
+
+ try {
+ // Wipe the encoded key bytes
+ secureWipeBytes(key.encoded)
+ Log.d(TAG, "Securely wiped SecretKey from memory")
+ } catch (e: Exception) {
+ Log.w(TAG, "Warning: Could not securely wipe SecretKey from memory", e)
+ }
}
override fun onCreate(savedInstanceState: Bundle?) {
@@ -92,128 +154,223 @@ class MainActivity : AppCompatActivity() {
}
// Get new Firebase registration token
- val token = task.result
- Log.d(TAG, "Firebase Registration Token: $token")
+ var token = task.result
+ Log.d(TAG, "Firebase Registration Token received (${token.length} characters)")
- // Send token to server
- sendTokenToServer(token)
+ try {
+ // Send token to server
+ sendTokenToServer(token)
+ } finally {
+ // Wipe token from memory after sending
+ token = secureWipeString(token)
+ }
}
}
private fun sendTokenToServer(token: String) {
- updateStatus(getString(R.string.status_encrypting_token))
-
- // Encrypt the token before sending
- val encryptedToken = try {
- encryptToken(token)
- } catch (e: Exception) {
- Log.e(TAG, "Failed to encrypt token", e)
- updateStatus(getString(R.string.status_failed_encrypt, e.message ?: ""))
- registerButton.isEnabled = true
- return
- }
-
- val json = JSONObject()
- json.put("encrypted_data", encryptedToken)
- json.put("platform", "android")
-
- val body = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
+ var secureToken = token // Create mutable copy for secure wiping
- val backendUrl = SettingsActivity.getBackendUrl(this@MainActivity)
- val registerUrl = "$backendUrl/register"
-
- val request = Request.Builder()
- .url(registerUrl)
- .post(body)
- .build()
-
- client.newCall(request).enqueue(object : Callback {
- override fun onFailure(call: Call, e: IOException) {
- Log.e(TAG, "Failed to send token to server", e)
- runOnUiThread {
- updateStatus(getString(R.string.status_failed_register, e.message ?: ""))
- registerButton.isEnabled = true
- }
+ try {
+ updateStatus(getString(R.string.status_encrypting_token))
+
+ // Encrypt the token before sending
+ val encryptedToken = try {
+ encryptToken(secureToken)
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to encrypt token", e)
+ updateStatus(getString(R.string.status_failed_encrypt, e.message ?: ""))
+ registerButton.isEnabled = true
+ return
+ } finally {
+ // Wipe token from memory immediately after encryption attempt
+ secureToken = secureWipeString(secureToken)
}
- override fun onResponse(call: Call, response: Response) {
- val responseBody = response.body?.string()
- Log.d(TAG, "Server response: ${response.code} - $responseBody")
+ val json = JSONObject()
+ json.put("encrypted_data", encryptedToken)
+ json.put("platform", "android")
+
+ val body = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
+
+ val backendUrl = SettingsActivity.getBackendUrl(this@MainActivity)
+ val registerUrl = "$backendUrl/register"
+
+ val request = Request.Builder()
+ .url(registerUrl)
+ .post(body)
+ .build()
+
+ client.newCall(request).enqueue(object : Callback {
+ override fun onFailure(call: Call, e: IOException) {
+ Log.e(TAG, "Failed to send token to server", e)
+ runOnUiThread {
+ updateStatus(getString(R.string.status_failed_register, e.message ?: ""))
+ registerButton.isEnabled = true
+ }
+ }
- runOnUiThread {
- if (response.isSuccessful) {
- updateStatus(getString(R.string.status_success))
- } else {
- updateStatus(getString(R.string.status_server_error, response.code, responseBody))
+ override fun onResponse(call: Call, response: Response) {
+ val responseBody = response.body?.string()
+ Log.d(TAG, "Server response: ${response.code} - $responseBody")
+
+ runOnUiThread {
+ if (response.isSuccessful) {
+ updateStatus(getString(R.string.status_success))
+ } else {
+ updateStatus(getString(R.string.status_server_error, response.code, responseBody))
+ }
+ registerButton.isEnabled = true
}
- registerButton.isEnabled = true
}
- }
- })
+ })
+
+ } finally {
+ // Ensure token is wiped from memory even if there are exceptions
+ secureToken = secureWipeString(secureToken)
+ }
}
private fun updateStatus(message: String) {
statusText.text = message
}
- private fun createUnsafeOkHttpClient(): OkHttpClient {
+ /**
+ * Creates HTTP client with build-appropriate certificate validation
+ * - Debug builds: Allow self-signed certificates for development
+ * - Release builds: Strict certificate validation using system CAs only
+ */
+ private fun createHttpClient(): OkHttpClient {
+ return if (isDebugBuild()) {
+ createDebugHttpClient()
+ } else {
+ createReleaseHttpClient()
+ }
+ }
+
+ /**
+ * Determines if this is a debug build by checking application info
+ * Falls back to safe release behavior if detection fails
+ */
+ private fun isDebugBuild(): Boolean {
+ // Manual override for testing (WARNING: Never use in production)
+ if (FORCE_DEBUG_CERTIFICATES) {
+ Log.w(TAG, "WARNING: Using forced debug certificate mode - not for production!")
+ return true
+ }
+
return try {
- // Create a trust manager that does not validate certificate chains
+ // Check application debuggable flag
+ val appInfo = packageManager.getApplicationInfo(packageName, 0)
+ val isDebuggable = (appInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
+
+ Log.i(TAG, "Build type detection - Debuggable flag: $isDebuggable")
+ return isDebuggable
+ } catch (e: Exception) {
+ Log.e(TAG, "Error checking debug build status, defaulting to release mode for security", e)
+ false // Default to release behavior for safety
+ }
+ }
+
+ /**
+ * Debug HTTP client - allows self-signed certificates for development
+ * WARNING: Only used in debug builds, never in production
+ */
+ private fun createDebugHttpClient(): OkHttpClient {
+ return try {
+ Log.w(TAG, "DEBUG BUILD: Using development HTTP client that accepts self-signed certificates")
+
+ // Create a trust manager that accepts self-signed certificates
val trustAllCerts = arrayOf(
object : X509TrustManager {
- override fun checkClientTrusted(chain: Array, authType: String) {}
- override fun checkServerTrusted(chain: Array, authType: String) {}
+ override fun checkClientTrusted(chain: Array, authType: String) {
+ Log.d(TAG, "Debug: Accepting client certificate: ${chain[0].subjectDN}")
+ }
+ override fun checkServerTrusted(chain: Array, authType: String) {
+ Log.d(TAG, "Debug: Accepting server certificate: ${chain[0].subjectDN}")
+ }
override fun getAcceptedIssuers(): Array = arrayOf()
}
)
- // Install the all-trusting trust manager
- val sslContext = SSLContext.getInstance("SSL")
- sslContext.init(null, trustAllCerts, java.security.SecureRandom())
-
- // Create an ssl socket factory with our all-trusting manager
- val sslSocketFactory = sslContext.socketFactory
+ val sslContext = SSLContext.getInstance("TLS")
+ sslContext.init(null, trustAllCerts, SecureRandom())
OkHttpClient.Builder()
- .sslSocketFactory(sslSocketFactory, trustAllCerts[0] as X509TrustManager)
- .hostnameVerifier(HostnameVerifier { _, _ -> true })
+ .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
+ .hostnameVerifier { hostname, _ ->
+ Log.d(TAG, "Debug: Accepting hostname: $hostname")
+ true
+ }
.build()
} catch (e: Exception) {
- Log.e(TAG, "Error creating unsafe HTTP client", e)
- OkHttpClient()
+ Log.e(TAG, "Error creating debug HTTP client", e)
+ OkHttpClient() // Fallback to default client
}
}
- private fun encryptToken(token: String): String {
- // Generate a random AES-256 key for this token
- val keyGen = KeyGenerator.getInstance("AES")
- keyGen.init(256)
- val aesKey = keyGen.generateKey()
-
- // Encrypt the token with AES-GCM
- val aesCipher = Cipher.getInstance("AES/GCM/NoPadding")
- val iv = ByteArray(12) // 96-bit IV for GCM
- SecureRandom().nextBytes(iv)
- val gcmSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag
- aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec)
-
- val encryptedToken = aesCipher.doFinal(token.toByteArray())
+ /**
+ * Release HTTP client - strict certificate validation
+ * Uses system certificate authorities only for production security
+ */
+ private fun createReleaseHttpClient(): OkHttpClient {
+ Log.i(TAG, "RELEASE BUILD: Using secure HTTP client with strict certificate validation")
- // Encrypt the AES key with RSA
- val publicKey = loadPublicKey()
- val rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
- rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey)
- val encryptedAesKey = rsaCipher.doFinal(aesKey.encoded)
-
- // Combine: IV (12 bytes) + encrypted AES key length (4 bytes) + encrypted AES key + encrypted token
- val keyLengthBytes = ByteArray(4)
- keyLengthBytes[0] = (encryptedAesKey.size shr 24).toByte()
- keyLengthBytes[1] = (encryptedAesKey.size shr 16).toByte()
- keyLengthBytes[2] = (encryptedAesKey.size shr 8).toByte()
- keyLengthBytes[3] = encryptedAesKey.size.toByte()
+ // Use default OkHttpClient which respects network security config
+ // This will enforce the release network_security_config.xml settings
+ return OkHttpClient.Builder()
+ .build()
+ }
+
+ private fun encryptToken(token: String): String {
+ var aesKey: SecretKey? = null
+ var iv: ByteArray? = null
+ var encryptedToken: ByteArray? = null
+ var encryptedAesKey: ByteArray? = null
- val combined = iv + keyLengthBytes + encryptedAesKey + encryptedToken
- return Base64.encodeToString(combined, Base64.DEFAULT)
+ try {
+ // Generate a random AES-256 key for this token
+ val keyGen = KeyGenerator.getInstance("AES")
+ keyGen.init(256)
+ aesKey = keyGen.generateKey()
+
+ // Encrypt the token with AES-GCM
+ val aesCipher = Cipher.getInstance("AES/GCM/NoPadding")
+ iv = ByteArray(12) // 96-bit IV for GCM
+ SecureRandom().nextBytes(iv)
+ val gcmSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag
+ aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, gcmSpec)
+
+ encryptedToken = aesCipher.doFinal(token.toByteArray())
+
+ // Encrypt the AES key with RSA
+ val publicKey = loadPublicKey()
+ val rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding")
+ rsaCipher.init(Cipher.ENCRYPT_MODE, publicKey)
+ encryptedAesKey = rsaCipher.doFinal(aesKey.encoded)
+
+ // Combine: IV (12 bytes) + encrypted AES key length (4 bytes) + encrypted AES key + encrypted token
+ val keyLengthBytes = ByteArray(4)
+ keyLengthBytes[0] = (encryptedAesKey.size shr 24).toByte()
+ keyLengthBytes[1] = (encryptedAesKey.size shr 16).toByte()
+ keyLengthBytes[2] = (encryptedAesKey.size shr 8).toByte()
+ keyLengthBytes[3] = encryptedAesKey.size.toByte()
+
+ val combined = iv + keyLengthBytes + encryptedAesKey + encryptedToken
+ return Base64.encodeToString(combined, Base64.DEFAULT)
+
+ } finally {
+ // Securely wipe sensitive data from memory
+ try {
+ aesKey?.let { secureWipeSecretKey(it) }
+ iv?.let { secureWipeBytes(it) }
+ encryptedToken?.let { secureWipeBytes(it) }
+ encryptedAesKey?.let { secureWipeBytes(it) }
+
+ Log.d(TAG, "Encryption completed, sensitive data wiped from memory")
+ } catch (e: Exception) {
+ Log.w(TAG, "Warning: Could not completely wipe encryption data from memory", e)
+ }
+ }
}
private fun loadPublicKey(): PublicKey {
diff --git a/demo-app/app/src/release/res/xml/network_security_config.xml b/demo-app/app/src/release/res/xml/network_security_config.xml
new file mode 100644
index 0000000..3b99b63
--- /dev/null
+++ b/demo-app/app/src/release/res/xml/network_security_config.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+ your-production-domain.com
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo-app/build.gradle b/demo-app/build.gradle
index b7225ac..93c5151 100644
--- a/demo-app/build.gradle
+++ b/demo-app/build.gradle
@@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = "1.9.10"
+ ext.kotlin_version = "2.2.0"
repositories {
google()
mavenCentral()
diff --git a/notification-backend/go.mod b/notification-backend/go.mod
index 06eecc8..7f39ead 100644
--- a/notification-backend/go.mod
+++ b/notification-backend/go.mod
@@ -1,22 +1,22 @@
module notification-backend
-go 1.24.3
+go 1.24.5
require (
- firebase.google.com/go/v4 v4.16.1
+ firebase.google.com/go/v4 v4.17.0
github.com/aws/aws-sdk-go-v2 v1.36.6
github.com/aws/aws-sdk-go-v2/config v1.29.18
github.com/aws/aws-sdk-go-v2/credentials v1.17.71
github.com/aws/aws-sdk-go-v2/service/s3 v1.84.1
- google.golang.org/api v0.231.0
+ google.golang.org/api v0.243.0
)
require (
cel.dev/expr v0.23.1 // indirect
cloud.google.com/go v0.121.0 // indirect
- cloud.google.com/go/auth v0.16.1 // indirect
+ cloud.google.com/go/auth v0.16.3 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
- cloud.google.com/go/compute/metadata v0.6.0 // indirect
+ cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/firestore v1.18.0 // indirect
cloud.google.com/go/iam v1.5.2 // indirect
cloud.google.com/go/longrunning v0.6.7 // indirect
@@ -53,30 +53,30 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
- github.com/googleapis/gax-go/v2 v2.14.1 // indirect
+ github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
- go.opentelemetry.io/otel v1.35.0 // indirect
- go.opentelemetry.io/otel/metric v1.35.0 // indirect
- go.opentelemetry.io/otel/sdk v1.35.0 // indirect
- go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
- go.opentelemetry.io/otel/trace v1.35.0 // indirect
- golang.org/x/crypto v0.38.0 // indirect
- golang.org/x/net v0.40.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
+ go.opentelemetry.io/otel v1.36.0 // indirect
+ go.opentelemetry.io/otel/metric v1.36.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.36.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
+ go.opentelemetry.io/otel/trace v1.36.0 // indirect
+ golang.org/x/crypto v0.40.0 // indirect
+ golang.org/x/net v0.42.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
- golang.org/x/sync v0.14.0 // indirect
- golang.org/x/sys v0.33.0 // indirect
- golang.org/x/text v0.25.0 // indirect
- golang.org/x/time v0.11.0 // indirect
+ golang.org/x/sync v0.16.0 // indirect
+ golang.org/x/sys v0.34.0 // indirect
+ golang.org/x/text v0.27.0 // indirect
+ golang.org/x/time v0.12.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
- google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect
- google.golang.org/grpc v1.72.0 // indirect
+ google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 // indirect
+ google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)
diff --git a/notification-backend/go.sum b/notification-backend/go.sum
index 622139b..7954745 100644
--- a/notification-backend/go.sum
+++ b/notification-backend/go.sum
@@ -2,12 +2,12 @@ cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg=
cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q=
-cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU=
-cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
+cloud.google.com/go/auth v0.16.3 h1:kabzoQ9/bobUmnseYnBO6qQG7q4a/CffFRlJSxv2wCc=
+cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
-cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
-cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
+cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
+cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8=
@@ -22,8 +22,8 @@ cloud.google.com/go/storage v1.53.0 h1:gg0ERZwL17pJ+Cz3cD2qS60w1WMDnwcm5YPAIQBHU
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
-firebase.google.com/go/v4 v4.16.1 h1:Kl5cgXmM0VOWDGT1UAx6b0T2UFWa14ak0CvYqeI7Py4=
-firebase.google.com/go/v4 v4.16.1/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM=
+firebase.google.com/go/v4 v4.17.0 h1:Bih69QV/k0YKPA1qUX04ln0aPT9IERrAo2ezibcngzE=
+firebase.google.com/go/v4 v4.17.0/go.mod h1:aAPJq/bOyb23tBlc1K6GR+2E8sOGAeJSc8wIJVgl9SM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU=
@@ -110,8 +110,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
-github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
-github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
+github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=
+github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -127,72 +127,72 @@ go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJyS
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
-go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
-go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
+go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
+go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0 h1:PB3Zrjs1sG1GBX51SXyTSoOTqcDglmsk7nT6tkKPb/k=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.35.0/go.mod h1:U2R3XyVPzn0WX7wOIypPuptulsMcPDPs/oiSVOMVnHY=
-go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
-go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
-go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
-go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
-go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
-go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
-go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
-go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
+go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
+go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
+go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
+go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
+go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
+go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
+go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
+go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
-golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
+golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
+golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
+golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
-golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
+golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
-golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
-golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
+golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
+golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
+golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
+golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q=
-google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A=
+google.golang.org/api v0.243.0 h1:sw+ESIJ4BVnlJcWu9S+p2Z6Qq1PjG77T8IJ1xtp4jZQ=
+google.golang.org/api v0.243.0/go.mod h1:GE4QtYfaybx1KmeHMdBnNnyLzBZCVihGBXAmJu/uUr8=
google.golang.org/appengine/v2 v2.0.6 h1:LvPZLGuchSBslPBp+LAhihBeGSiRh1myRoYK4NtuBIw=
google.golang.org/appengine/v2 v2.0.6/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 h1:1tXaIXCracvtsRxSBsYDiSBN0cuJvM7QYW+MrpIRY78=
-google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:49MsLSx0oWMOZqcpB3uL8ZOkAh1+TndpJ8ONoCBWiZk=
-google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2 h1:vPV0tzlsK6EzEDHNNH5sa7Hs9bd7iXR7B1tSiPepkV0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:pKLAc5OolXC3ViWGI62vvC0n10CpwAtRcTNCFwTKBEw=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
-google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
-google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
+google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 h1:oWVWY3NzT7KJppx2UKhKmzPq4SRe0LdCijVRwvGeikY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822/go.mod h1:h3c4v36UTKzUiuaOKQ6gr3S+0hovBtUrXzTG/i3+XEc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79 h1:1ZwqphdOdWYXsUHgMpU/101nCtf/kSp9hOrcvFsnl10=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250715232539-7130f93afb79/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
diff --git a/notification-backend/main.go b/notification-backend/main.go
index a757f58..9f79c69 100644
--- a/notification-backend/main.go
+++ b/notification-backend/main.go
@@ -17,6 +17,7 @@ import (
"log"
"net/http"
"os"
+ "strings"
"sync"
"time"
@@ -209,6 +210,37 @@ func (ts *DurableTokenStore) saveToFile() error {
return os.Rename(tempFile, ts.storageFile)
}
+// RequestLog represents a structured log entry for HTTP requests
+type RequestLog struct {
+ Timestamp time.Time `json:"timestamp"`
+ Method string `json:"method"`
+ Path string `json:"path"`
+ RemoteAddr string `json:"remote_addr"`
+ UserAgent string `json:"user_agent"`
+ StatusCode int `json:"status_code"`
+ ResponseTime int64 `json:"response_time_ms"`
+ BodySize int64 `json:"body_size"`
+ Error string `json:"error,omitempty"`
+}
+
+// ResponseWriter wrapper to capture status code and response size
+type loggingResponseWriter struct {
+ http.ResponseWriter
+ statusCode int
+ bodySize int64
+}
+
+func (lrw *loggingResponseWriter) WriteHeader(code int) {
+ lrw.statusCode = code
+ lrw.ResponseWriter.WriteHeader(code)
+}
+
+func (lrw *loggingResponseWriter) Write(b []byte) (int, error) {
+ size, err := lrw.ResponseWriter.Write(b)
+ lrw.bodySize += int64(size)
+ return size, err
+}
+
var (
tokenStore *DurableTokenStore
exoscaleStorage *ExoscaleStorage
@@ -218,6 +250,75 @@ var (
useExoscale bool
)
+// loggingMiddleware wraps HTTP handlers to provide structured logging
+func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ start := time.Now()
+
+ // Create logging response writer
+ lrw := &loggingResponseWriter{
+ ResponseWriter: w,
+ statusCode: 200, // Default status code
+ }
+
+ // Call the next handler
+ next(lrw, r)
+
+ // Calculate response time
+ responseTime := time.Since(start).Milliseconds()
+
+ // Create structured log entry
+ logEntry := RequestLog{
+ Timestamp: start,
+ Method: r.Method,
+ Path: r.URL.Path,
+ RemoteAddr: getClientIP(r),
+ UserAgent: r.UserAgent(),
+ StatusCode: lrw.statusCode,
+ ResponseTime: responseTime,
+ BodySize: lrw.bodySize,
+ }
+
+ // Add error field for non-2xx responses
+ if lrw.statusCode >= 400 {
+ logEntry.Error = http.StatusText(lrw.statusCode)
+ }
+
+ // Log as JSON
+ logJSON, err := json.Marshal(logEntry)
+ if err != nil {
+ log.Printf("Error marshaling log entry: %v", err)
+ return
+ }
+
+ log.Printf("REQUEST_LOG: %s", string(logJSON))
+ }
+}
+
+// getClientIP extracts the real client IP from request headers
+func getClientIP(r *http.Request) string {
+ // Check X-Forwarded-For header (for proxies/load balancers)
+ if xForwardedFor := r.Header.Get("X-Forwarded-For"); xForwardedFor != "" {
+ // X-Forwarded-For can contain multiple IPs, take the first one
+ ifs := strings.Split(xForwardedFor, ",")
+ if len(ifs) > 0 {
+ return strings.TrimSpace(ifs[0])
+ }
+ }
+
+ // Check X-Real-IP header (for nginx)
+ if xRealIP := r.Header.Get("X-Real-IP"); xRealIP != "" {
+ return xRealIP
+ }
+
+ // Fall back to RemoteAddr
+ // Remove port if present
+ if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
+ return r.RemoteAddr[:idx]
+ }
+ return r.RemoteAddr
+}
+
func main() {
flag.Parse()
@@ -294,11 +395,11 @@ func main() {
go startCleanupRoutine()
}
- http.HandleFunc("/register", handleRegister)
- http.HandleFunc("/send", handleSend)
- http.HandleFunc("/notify", handleNotify)
- http.HandleFunc("/status", handleStatus)
- http.HandleFunc("/", handleRoot)
+ http.HandleFunc("/register", loggingMiddleware(handleRegister))
+ http.HandleFunc("/send", loggingMiddleware(handleSend))
+ http.HandleFunc("/notify", loggingMiddleware(handleNotify))
+ http.HandleFunc("/status", loggingMiddleware(handleStatus))
+ http.HandleFunc("/", loggingMiddleware(handleRoot))
log.Printf("FCM Notification Server starting on port %s", *port)
log.Printf("Storage: %s", getStorageType())