When Time-Traveler Meets SSL Certificate - Fixing Time Sync Problem

When Time-Traveler Meets SSL Certificate - Fixing Time Sync Problem

Hey there, fellow developers and tech enthusiasts! 🙋‍♂️

Ever had one of those days when something goes hilariously wrong, but in the end, you learn something super valuable? Well, that’s exactly what happened with our app, Flickerwall, during a recent client call. Our app couldn’t connect to the server, and after a lot of head-scratching, we finally discovered the cause: a time traveler glitch!

Yes, you read that right. The client’s system time was completely out of sync with the server’s time — almost like they were stuck somewhere in 2018! 😅 This mismatch led to SSL certificate errors, and as you can guess, SSL certificates are very picky about device time.

Why Does Time Matter?

For those who might be new to SSL (Secure Sockets Layer) certificates, let me explain. SSL certificates are what keep communication between your app and the server secure. They encrypt your data, ensuring that no one can spy on it. But, here’s the catch—SSL certificates are only valid within a specific time window. If the system’s clock is not in sync, the certificate gets rejected. It’s like trying to use an expired movie ticket means no entry!

Think of SSL certificates like VIP passes for your app to securely talk to the server. They have a specific time window in which they’re valid, and if the device’s clock is even a bit off, you’ll be shown an error. This could be a real headache, especially when it’s something as simple as a clock being wrong.

If the system’s time isn’t within the certificate’s validity range, you’ll face an SSL error. Simple, but annoying when it happens.

How we can handle this issue

So we know the reason behind it let’s figure out some ways to handle this issue, although this is not a bug but this could hamper user’s experience and we should have feature to detect time out of sync issue.

1. Using GPS Time

for devices with GPS we could use the GPS time. GPS time is highly accurate and can help in scenarios where network time may be unreliable. However, it requires location services to be enabled, which might not be ideal for all users and could raise privacy concerns.

class MainActivity : ComponentActivity(), LocationListener {
    companion object {
        private const val TIME_SYNC_THRESHOLD = 1000L // 1 second
    }
    private lateinit var locationManager: LocationManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager

  //assuming all permissions granted (FINE_LOCATION, COARSE_LOCATION)
        startLocationUpdates()
    }

    private fun startLocationUpdates() {
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this, Looper.getMainLooper())
    }

    override fun onLocationChanged(location: Location) {
        val gpsTime = location.time
        val clientTime = System.currentTimeMillis()
        
        if (Math.abs(gpsTime - clientTime) > TIME_SYNC_THRESHOLD) {
            // Show dialog on main thread
            Handler(Looper.getMainLooper()).post {
                showTimeSyncDialog()
            }
        }
        
        // Stop receiving updates once we have the time
        locationManager.removeUpdates(this)
    }

    private fun showTimeSyncDialog() {
        //  logic to show dialog
    }
}

2. Server-Side Time Synchronisation

We could also set up a time sync endpoint on the server. This way, the client can check the server’s time to avoid discrepancies. Make sure that this endpoint is accessible only through HTTP.

Here’s a quick server-side example using Node.js and Express:

const express = require('express');
const app = express();

app.get('/server-time', (req, res) => {
    res.json({ serverTime: new Date().getTime() });
});

app.listen(5000, () => {
    console.log('Server running on port 3000');
});

3. Peer-to-Peer Time Sync

If your app involves multiple users or devices to communicate each other, we can consider implementing a peer-to-peer time synchronisation method where devices can compare and adjust their time based on other data, But In this approach, it can be challenging to determine which users have the correct time, and additional logic is needed to discard incorrect time.

How We Fixed It

So, what did we do to resolve this? Here’s the step-by-step process we followed:

  1. Server Time Check using NTP: We implemented a feature that checks the server’s time using the Network Time Protocol (NTP) before establishing a connection with backend.
  2. Time Difference Detection: We compared the client’s time with the server’s time. If the difference was more than a set threshold (20 seconds), we showed a pop-up dialog telling the user that their time was out of sync and needed to be fixed

To implement this in your own app, here’s how we did it:

1: Add Dependencies

First, add this NTP library to your `build.gradle

implementation 'org.apache.commons:commons-net:3.8.0'

2: Create a Utility Class for NTP

Create a utility class to fetch the server time via NTP:

import org.apache.commons.net.time.TimeTCPClient
import java.io.IOException
import java.net.InetAddress

object NTPClient {
    private const val NTP_SERVER = "pool.ntp.org" // NTP server address, 
    //other alternatives: time.google.com,time.apple.com, ntp.ubuntu.com 

    @Throws(IOException::class.java)
    fun getServerTime(): Long {
        val timeClient = TimeTCPClient()
        timeClient.defaultTimeout = 1000
        timeClient.connect(InetAddress.getByName(NTP_SERVER))
        return timeClient.date.time
    }
}

3: Check Time and Show Dialog

In your activity or fragment, check if the client time and server time are synced.

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.* // Make sure you import these

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Use lifecycleScope to launch the coroutine within the Activity's lifecycle
        lifecycleScope.launch(Dispatchers.IO) { // Use Dispatchers.IO for background work
            try {
                val serverTime = NTPClient.getServerTime()
                val clientTime = System.currentTimeMillis()
                if (Math.abs(serverTime - clientTime) > TIME_SYNC_THRESHOLD) {
                    // Show dialog on the main thread
                    withContext(Dispatchers.Main) { // Switch back to the main thread
                        showTimeSyncDialog()
                    }
                }
            } catch (e: Exception) { // Catch general exceptions
                e.printStackTrace()
            }
        }
    }

    private fun showTimeSyncDialog() {
        // Logic to handle the dialog box
    }
    
    companion object {
        private const val TIME_SYNC_THRESHOLD = 20000L // time in milliseconds
    }
}

So there you have it! What started as a confusing and frustrating issue turned into a valuable lesson about the importance of time synchronisation. Remember, in the world of tech, time isn’t just money—it’s the key to smooth experience and happy users.

Next time you’re debugging and facing connectivity issues, don’t forget to check if your clocks are ticking in harmony.

Happy coding, and may your clocks always be in sync! 🕰️

If you like this article, don't forget to share it with your friends and colleagues.