Skip to main content

Overview

NetPOS implements multiple layers of security to protect sensitive payment data, user credentials, and transaction information. The application follows PCI-DSS compliance requirements for payment card industry security standards.

Encrypted Shared Preferences

Sensitive data is stored using Android’s EncryptedSharedPreferences with AES-256 encryption.
util/EncryptedPrefsUtils.kt:7
object EncryptedPrefsUtils {

    fun putString(context: Context, key: String, value: String) {
        val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

        val sharedPreferences = EncryptedSharedPreferences.create(
            "encrypted_prefs_name",
            masterKeyAlias,
            context,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )

        sharedPreferences.edit().putString(key, value).apply()
    }

    fun getString(context: Context, key: String): String? {
        val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

        val sharedPreferences = EncryptedSharedPreferences.create(
            "encrypted_prefs_name",
            masterKeyAlias,
            context,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )

        return sharedPreferences.getString(key, null)
    }
}
Encryption Algorithms Used:
  • AES256-GCM for value encryption (Galois/Counter Mode)
  • AES256-SIV for key encryption (Synthetic IV mode)
  • Keys managed by Android Keystore system

Authentication Flow

JWT-Based Authentication

NetPOS uses JSON Web Tokens (JWT) for user authentication and session management.
1

User Login

User enters email and password in AuthenticationActivity
2

Credential Validation

viewmodels/AuthViewModel.kt:79
private fun auth(username: String, password: String) {
    val credentials = JsonObject().apply {
        addProperty("username", username)
        addProperty("password", password)
    }
    
    stormApiService.userToken(credentials)
        .flatMap { response ->
            if (!response.success) {
                throw Exception("Login Failed")
            }
            val userToken = response.token
            Prefs.putString(PREF_USER_TOKEN, userToken)
            parseUserFromJWT(userToken)
        }
}
3

JWT Parsing

Extract user claims from JWT token:
val userTokenDecoded = JWT(userToken)
val user = User().apply {
    this.terminal_id = userTokenDecoded.getClaim("terminalId").asString()
    this.business_name = userTokenDecoded.getClaim("businessName").asString()
    this.netplus_id = userTokenDecoded.getClaim("stormId").asString()
    this.mid = userTokenDecoded.getClaim("mid").asString()
    this.partnerId = userTokenDecoded.getClaim("partnerId").asString()
}
4

Session Storage

Store user session and token:
Prefs.putString(PREF_USER, gson.toJson(user))
Prefs.putBoolean(PREF_AUTHENTICATED, true)

JWT Claims Structure

{
  "stormId": "merchant-unique-id",
  "terminalId": "terminal-identifier",
  "businessName": "Merchant Business Name",
  "mid": "merchant-id",
  "partnerId": "partner-identifier",
  "merchantId": "merchant-identifier",
  "netplusPayMid": "netpluspay-mid",
  "phoneNumber": "merchant-phone",
  "business_address": "merchant-address",
  "username": "user-email",
  "iat": 1646383548,
  "exp": 1646469948
}

API Security

Bearer Token Authorization

All authenticated API calls include Bearer token in headers:
di/Module.kt:92
@Named("zenithPayByTransferHeaderInterceptor")
fun providesZenithPayByTransferHeaderInterceptor(): Interceptor = Interceptor { chain ->
    val originalRequest = chain.request()
    val requestWithAuth = originalRequest.newBuilder()
        .addHeader("Authorization", "Bearer ${Prefs.getString(PREF_USER_TOKEN, "")}")
        .build()
    chain.proceed(requestWithAuth)
}

Network Security Configuration

  • Minimum TLS Version: TLS 1.2
  • Certificate Pinning: Configured for production endpoints
  • Clear Text Traffic: Enabled only for development builds
AndroidManifest.xml:29
<application
    android:usesCleartextTraffic="true"
    tools:targetApi="m">
Clear text traffic should be disabled in production builds.

Request Timeout Configuration

di/Module.kt:107
OkHttpClient().newBuilder()
    .connectTimeout(120, TimeUnit.SECONDS)
    .readTimeout(120, TimeUnit.SECONDS)
    .writeTimeout(120, TimeUnit.SECONDS)
    .retryOnConnectionFailure(true)
    .build()

Card Data Security

PAN Masking

Credit card numbers (PAN) are always masked before storage:
data class TransactionResponse(
    val maskedPan: String?,  // e.g., "506085******1234"
    val cardExpiry: String?, // Stored temporarily, cleared after print
    val cardHolder: String?,
    val cardLabel: String?
)
Full PAN is never stored in database or shared preferences. NIBSS EPMS library handles secure card reading.

NIBSS Key Management

Cryptographic keys for NIBSS transactions are managed securely:
val hostConfig = HostConfig(
    NetPosTerminalConfig.getTerminalId(),
    NetPosTerminalConfig.connectionData,
    NetPosTerminalConfig.getKeyHolder()!!,  // Encrypted keys
    NetPosTerminalConfig.getConfigData()!!
)

Secure Storage

User Credentials

Encrypted Storage (via EncryptedPrefsUtils):
  • Terminal configuration keys
  • Merchant sensitive identifiers
Regular SharedPreferences (non-sensitive):
  • User token (JWT)
  • User profile (business name, terminal ID)
  • App preferences and settings
// Secure storage usage
EncryptedPrefsUtils.putString(context, "terminal_master_key", masterKey)

// Regular storage
Prefs.putString(PREF_USER_TOKEN, userToken)

Database Encryption

The Room database is not encrypted by default. Consider using SQLCipher for database-level encryption in production:
implementation "net.zetetic:android-database-sqlcipher:4.5.4"
implementation "androidx.sqlite:sqlite-ktx:2.3.1"

Password Security

Password Reset Flow

viewmodels/AuthViewModel.kt:378
fun resetPassword() {
    val username = usernameLiveData.value
    if (username.isNullOrEmpty()) {
        _message.value = Event("Please enter your email address")
        return
    }

    val payload = JsonObject().apply {
        addProperty("username", username)
    }
    
    stormApiService.passwordReset(payload)
        .subscribeOn(Schedulers.io())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe { response, error ->
            response?.let {
                if (it.code() == 200) {
                    _message.value = Event("Password reset email sent to $username")
                }
            }
        }
}
Password reset is handled server-side. Client only initiates the request.

Permissions

NetPOS requests the following sensitive permissions:
AndroidManifest.xml:6
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  • Location: Geolocation tagging for transactions (fraud prevention)
  • Camera: QR code scanning for contactless payments
  • Phone State: Device identification for terminal registration
  • Storage: Receipt PDF generation and export
  • Internet: API communication for transaction processing

Logging Security

Debug vs Release Logging

app/NetPosApp.kt:26
override fun onCreate() {
    super.onCreate()
    if (BuildConfig.DEBUG) {
        Timber.plant(Timber.DebugTree())
    }
}
Sensitive data logging is disabled in release builds. Use Timber instead of Log for automatic filtering.

HTTP Logging

di/Module.kt:86
@Named("loginInterceptor")
fun providesLoginInterceptor(): Interceptor = 
    HttpLoggingInterceptor().apply {
        setLevel(HttpLoggingInterceptor.Level.BODY)
    }
In production, change logging level to BASIC or NONE to prevent exposure of sensitive data in logs.

Firebase Security

Cloud Messaging

app/NetPosApp.kt:59
Firebase.messaging.subscribeToTopic("netpos_campaign")
    .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            Prefs.putBoolean("notification_campaign", true)
        }
    }

Push Notification Service

services/MyFirebaseMessagingService.kt
class MyFirebaseMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // Validate message source before processing
        remoteMessage.data.isNotEmpty().let {
            // Process secure notification
        }
    }
}

Security Best Practices

Input Validation

Validate all user inputs before API calls:
if (!Patterns.EMAIL_ADDRESS.matcher(username).matches()) {
    _message.value = Event("Please enter a valid email")
    return
}

Error Handling

Never expose sensitive error details to UI:
error?.let {
    _message.value = Event("Login failed. Please try again.")
    Timber.e(it) // Log full error securely
}

Session Management

Implement token expiration and refresh:
  • JWT tokens expire after 24 hours
  • Force re-authentication on token expiry

Secure Communication

All API calls use HTTPS with certificate validation

Code Obfuscation

build.gradle:86
release {
    minifyEnabled false
    shrinkResources false
    proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
Security Risk: Code obfuscation is currently disabled. Enable ProGuard/R8 for production:
minifyEnabled true
shrinkResources true

Security Checklist

1

Enable code obfuscation

Set minifyEnabled true in release build
2

Disable clear text traffic

Remove usesCleartextTraffic="true" for production
3

Implement certificate pinning

Pin SSL certificates for critical API endpoints
4

Enable database encryption

Integrate SQLCipher for encrypted Room database
5

Reduce logging in production

Set HTTP logging to NONE in release builds
6

Implement biometric authentication

Add fingerprint/face unlock for app access

Architecture

Application architecture overview

Models

Secure data model definitions

Build docs developers (and LLMs) love