Secure JWT Storage in Android

Jyotishgher Astrology
By -
9 minute read
0

 Secure and Resilient JWT Storage Using Encrypted Shared Preferences in Android

In today's web development landscape, JSON Web Tokens (JWT) have become a popular choice for authentication and authorization. However, securely storing JSON web tokens in an application's frontend poses a significant challenge. 

In this article, we will explore various techniques to address this issue and ensure the protection of sensitive user information. 

We will cover the pros and cons of using LocalStorage and cookies and provide code snippets to implement these solutions effectively.

Secure JWT Storage in Android

What is a JSON web token (JWT token)?

Before delving into storage options, it's crucial to understand the nature of a JWT token - its an "open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object."

A JSON web token consists of three parts: a header (think authorization header), a JWT payload, and a signature. 

The payload contains claims, such as user information or permissions (eg. used as an access token), while the signature ensures the token's integrity.


1. Secure Storage Mechanisms

  • SharedPreferences (with Caution):

    • Use with caution!
    • Encryption: Encrypt the JWT before storing it in SharedPreferences using a strong encryption library like javax.crypto.
    • Key Management: Store the encryption key securely (e.g., in a separate, securely stored file).
    • Example (Conceptual):

      SharedPreferences sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE);
      SharedPreferences.Editor editor = sharedPreferences.edit();
      String encryptedToken = encryptJWT(jwtToken); // Encrypt the JWT
      editor.putString("jwt", encryptedToken);
      editor.apply(); 
      
  • Android Keystore:

    • A more secure option for storing sensitive data.
    • Keystore: Android's built-in secure storage system.
    • Encryption: Use Keystore to encrypt and decrypt the JWT.
    • Example (Conceptual):

      // Obtain a KeyStore instance
      KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 
      // ... (Key generation and storage logic) ...
      
      // Encrypt the JWT using the KeyStore key
      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 
      cipher.init(Cipher.ENCRYPT_MODE, keyStoreKey); 
      byte[] encryptedJwt = cipher.doFinal(jwtToken.getBytes());
      
      // Store the encrypted JWT in SharedPreferences (or another secure storage)
      // ...
      
  • Secure Encrypted Storage (Android 11 and above):

    • Provides a more secure and isolated storage mechanism.
    • Requires Android 11 or higher.

2. Important Security Considerations

  • Short Token Expiration: Set short expiration times for your JWTs to minimize the impact of potential compromises.
  • HTTP-Only Cookies (for Web Sessions): If using cookies for session management, set the HttpOnly flag to prevent JavaScript from accessing the cookie, making it harder for attackers to steal.
  • HTTPS: Always use HTTPS to encrypt communication between the client and server, protecting the JWT during transmission.
  • Regular Audits: Regularly review your security practices and update your code to address any vulnerabilities.
  • Avoid Storing Sensitive Data in the JWT: Keep sensitive information (like user passwords) out of the JWT.

3. Example (Conceptual - SharedPreferences with Encryption)


public class JwtHelper {

    private static final String ENCRYPTION_KEY = "your_strong_encryption_key"; 

    public static String encryptJWT(String jwt) {
        try {
            // Get an instance of the Cipher
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); 
            SecretKeySpec key = new SecretKeySpec(ENCRYPTION_KEY.getBytes(), "AES");
            IvParameterSpec iv = new IvParameterSpec(new byte[cipher.getBlockSize()]); 
            cipher.init(Cipher.ENCRYPT_MODE, key, iv);
            byte[] encrypted = cipher.doFinal(jwt.getBytes());
            return Base64.encodeToString(encrypted, Base64.DEFAULT); 
        } catch (Exception e) {
            Log.e("JwtHelper", "Error encrypting JWT: " + e.getMessage());
            return null; 
        }
    }

    public static String decryptJWT(String encryptedJwt) {
        // ... (Implement decryption logic similar to encryption) ... 
    }

    // ... (Methods for storing/retrieving encrypted JWT from SharedPreferences) ...
}

Disclaimer: This is a simplified example. Implementing secure JWT storage requires careful consideration of your specific security requirements and best practices.

Key Takeaways:

  • Choose a secure storage mechanism that best suits your needs.
  • Encrypt the JWT before storing it.
  • Use strong encryption algorithms and securely manage encryption keys.
  • Implement proper security measures throughout your application.

By following these guidelines, you can effectively secure JWTs in your Android applications and protect user data.

Let's Have an example in Android JAVA

If the RuntimeException: Error creating encrypted shared preferences persists, it suggests that there might be an issue with the underlying setup for EncryptedSharedPreferences. Let's address this systematically.

implementation "androidx.security:security-crypto:1.1.0-alpha06"
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;

import androidx.security.crypto.EncryptedSharedPreferences;
import androidx.security.crypto.MasterKey;

import java.io.IOException;
import java.security.GeneralSecurityException;


public class SecureSharedPreferencesUtils {
private static final String PREF_NAME = "MyAppPrefs";
private static final String JWT_KEY = "jwt_token";
private static SharedPreferences getSecureSharedPreferences(Context context) {
synchronized (SecureSharedPreferencesUtils.class) {
try {
Log.d("SecureSharedPreferences", "Initializing MasterKey...");
MasterKey masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();

Log.d("SecureSharedPreferences", "Creating EncryptedSharedPreferences...");
return EncryptedSharedPreferences.create(
context,
PREF_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
} catch (GeneralSecurityException | IOException e) {
Log.e("SecureSharedPreferences", "Error creating encrypted shared preferences", e);

// Handle corruption or other issues
clearCorruptedPreferences(context);

throw new RuntimeException("Error creating encrypted shared preferences: " + e.getMessage(), e);
}
}
}

public static void saveJwtToken(Context context, String token) {
try {
SharedPreferences sharedPreferences = getSecureSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(JWT_KEY, token);
editor.apply();
} catch (RuntimeException e) {
Log.e("SecureSharedPreferences", "Failed to save JWT token", e);
}
}

public static String getJwtToken(Context context) {
try {
SharedPreferences sharedPreferences = getSecureSharedPreferences(context);
return sharedPreferences.getString(JWT_KEY, null); // Return null if not found
} catch (RuntimeException e) {
Log.e("SecureSharedPreferences", "Failed to retrieve JWT token", e);
return null;
}
}

public static void saveMainPageJwtToken(Context context, String token) {
// Method reused for saving JWT token
saveJwtToken(context, token);
}

public static String getMainPageJwtToken(Context context) {
// Method reused for retrieving JWT token
return getJwtToken(context);
}

public static void clearJwtToken(Context context) {
try {
SharedPreferences sharedPreferences = getSecureSharedPreferences(context);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(JWT_KEY);
editor.apply();
} catch (RuntimeException e) {
Log.e("SecureSharedPreferences", "Failed to clear JWT token", e);
}
}

private static void clearCorruptedPreferences(Context context) {
Log.e("SecureSharedPreferences", "Clearing potentially corrupted preferences...");
try {
boolean deleted = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
deleted = context.deleteSharedPreferences(PREF_NAME);
}
if (deleted) {
Log.i("SecureSharedPreferences", "Corrupted preferences deleted successfully.");
} else {
Log.w("SecureSharedPreferences", "Failed to delete corrupted preferences.");
}
} catch (Exception e) {
Log.e("SecureSharedPreferences", "Error while clearing corrupted preferences", e);
}
}
}

Explanation of Changes

  1. Preserved Methods:

    • All original methods (saveJwtToken, getJwtToken, saveMainPageJwtToken, getMainPageJwtToken, clearJwtToken) remain unchanged in structure.
  2. Added Recovery Logic:

    • clearCorruptedPreferences deletes the shared preferences file if it is corrupted, ensuring the app can recover from corruption issues.
  3. Logging:

    • Detailed logging has been added at every critical step to identify where failures occur.
  4. Context Handling:

    • context is used directly to avoid issues with improper context.
Tags:

Post a Comment

0Comments

Post a Comment (0)