Mobile Integration Guide - PVPipe Biometric Authentication

Overview

This guide provides comprehensive instructions for integrating the PVPipe Biometric Authentication system into mobile applications for both iOS and Android platforms.

Table of Contents

  1. Architecture Overview
  2. iOS Integration
  3. Android Integration
  4. React Native Integration
  5. Security Best Practices
  6. Push Notifications
  7. Error Handling
  8. Testing Guide

Architecture Overview

Mobile Authentication Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚Mobile App   β”‚    β”‚ms-auth API   β”‚    β”‚Secure Hardware  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                   β”‚                     β”‚
   1.  β”‚ Request Challenge β”‚                     β”‚
       │──────────────────►│                     β”‚
       β”‚                   β”‚                     β”‚
   2.  │◄──────────────────│ Challenge + Session β”‚
       β”‚                   β”‚                     β”‚
   3.  β”‚ Biometric Prompt  β”‚                     β”‚
       │──────────────────────────────────────►  β”‚
       β”‚                   β”‚                     β”‚
   4.  │◄──────────────────────────────────────  β”‚
       β”‚           Signed Challenge              β”‚
       β”‚                   β”‚                     β”‚  
   5.  β”‚ Submit Signature  β”‚                     β”‚
       │──────────────────►│                     β”‚
       β”‚                   β”‚                     β”‚
   6.  │◄──────────────────│ JWT Tokens          β”‚
       β”‚                   β”‚                     β”‚

Key Components

  1. Secure Key Storage: Hardware Security Module (HSM) or Secure Enclave
  2. Biometric Authentication: TouchID, FaceID, Fingerprint, etc.
  3. Challenge-Response: Cryptographic proof of device ownership
  4. Token Management: Secure storage and automatic refresh
  5. Push Notifications: Real-time confirmation requests

iOS Integration

Prerequisites

  • iOS 12.0+ for biometric authentication
  • iOS 13.0+ for enhanced security features
  • Xcode 14.0+
  • Swift 5.0+

1. Setup Dependencies

Podfile

platform :ios, '13.0'

target 'PVPipeApp' do
  use_frameworks!
  
  # Firebase for push notifications
  pod 'Firebase/Messaging'
  
  # Keychain for secure storage
  pod 'KeychainAccess'
  
  # HTTP networking
  pod 'Alamofire'
  
  # JSON handling
  pod 'SwiftyJSON'
end

Package.swift

dependencies: [
    .package(url: "https://github.com/firebase/firebase-ios-sdk.git", from: "10.0.0"),
    .package(url: "https://github.com/kishikawakatsumi/KeychainAccess.git", from: "4.2.2"),
    .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0")
]

2. Biometric Key Management

BiometricKeyManager.swift

import Security
import LocalAuthentication
import CryptoKit

class BiometricKeyManager {
    private let keyTag = "com.pvpipe.biometric.key"
    private let keySize = 256
    
    enum BiometricError: Error {
        case biometricNotAvailable
        case biometricNotEnrolled
        case keyGenerationFailed
        case keyNotFound
        case signatureFailed
        case userCancel
    }
    
    // MARK: - Key Generation
    
    func generateKeyPair() async throws -> (publicKey: Data, keyAlgorithm: String) {
        guard canUseBiometrics() else {
            throw BiometricError.biometricNotAvailable
        }
        
        // Create access control for biometric authentication
        let accessControl = SecAccessControlCreateWithFlags(
            kCFAllocatorDefault,
            kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
            [.privateKeyUsage, .biometryAny],
            nil
        )
        
        guard accessControl != nil else {
            throw BiometricError.keyGenerationFailed
        }
        
        // Key generation parameters
        let keyAttributes: [String: Any] = [
            kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
            kSecAttrKeySizeInBits as String: keySize,
            kSecPrivateKeyAttrs as String: [
                kSecAttrIsPermanent as String: true,
                kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
                kSecAttrAccessControl as String: accessControl!
            ]
        ]
        
        var error: Unmanaged<CFError>?
        guard let privateKey = SecKeyCreateRandomKey(keyAttributes as CFDictionary, &error) else {
            throw BiometricError.keyGenerationFailed
        }
        
        // Get public key
        guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
            throw BiometricError.keyGenerationFailed
        }
        
        // Export public key in PEM format
        guard let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) else {
            throw BiometricError.keyGenerationFailed
        }
        
        let pemKey = try createPEMFromPublicKey(publicKeyData as Data)
        return (publicKey: pemKey, keyAlgorithm: "ES256")
    }
    
    // MARK: - Signature Operations
    
    func signChallenge(_ challenge: String) async throws -> String {
        let context = LAContext()
        context.localizedReason = "Authenticate to sign in to PVPipe"
        
        // Get private key from keychain
        let query: [String: Any] = [
            kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
            kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
            kSecReturnRef as String: true,
            kSecUseAuthenticationContext as String: context
        ]
        
        var item: CFTypeRef?
        let status = SecItemCopyMatching(query as CFDictionary, &item)
        
        guard status == errSecSuccess, let privateKey = item else {
            if status == errSecUserCancel {
                throw BiometricError.userCancel
            }
            throw BiometricError.keyNotFound
        }
        
        // Hash the challenge
        let challengeData = challenge.data(using: .utf8)!
        let hashedChallenge = SHA256.hash(data: challengeData)
        
        // Sign the hashed challenge
        var error: Unmanaged<CFError>?
        guard let signature = SecKeyCreateSignature(
            privateKey as! SecKey,
            .ecdsaSignatureMessageX962SHA256,
            Data(hashedChallenge) as CFData,
            &error
        ) else {
            throw BiometricError.signatureFailed
        }
        
        return (signature as Data).base64EncodedString()
    }
    
    // MARK: - Utility Methods
    
    private func canUseBiometrics() -> Bool {
        let context = LAContext()
        var error: NSError?
        
        return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
    }
    
    private func createPEMFromPublicKey(_ publicKeyData: Data) throws -> Data {
        // ASN.1 header for ECDSA P-256 public key
        let header = Data([
            0x30, 0x59, // SEQUENCE, length 89
            0x30, 0x13, // SEQUENCE, length 19
            0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02, 0x01, // OID for EC public key
            0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, // OID for P-256
            0x03, 0x42, 0x00 // BIT STRING, length 66, no unused bits
        ])
        
        let asn1Data = header + publicKeyData
        let base64String = asn1Data.base64EncodedString(options: [.lineLength64Characters, .endLineWithLineFeed])
        let pemString = "-----BEGIN PUBLIC KEY-----\n\(base64String)\n-----END PUBLIC KEY-----"
        
        return pemString.data(using: .utf8)!
    }
    
    func deleteKey() -> Bool {
        let query: [String: Any] = [
            kSecClass as String: kSecClassKey,
            kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!
        ]
        
        let status = SecItemDelete(query as CFDictionary)
        return status == errSecSuccess || status == errSecItemNotFound
    }
}

3. Device Registration

DeviceRegistrationService.swift

import Foundation
import UIKit

class DeviceRegistrationService {
    private let apiClient: APIClient
    private let keyManager: BiometricKeyManager
    private let deviceInfo: DeviceInfoProvider
    
    init(apiClient: APIClient, keyManager: BiometricKeyManager, deviceInfo: DeviceInfoProvider) {
        self.apiClient = apiClient
        self.keyManager = keyManager
        self.deviceInfo = deviceInfo
    }
    
    func registerDevice() async throws -> RegisteredDevice {
        // 1. Generate key pair
        let (publicKey, keyAlgorithm) = try await keyManager.generateKeyPair()
        
        // 2. Create device fingerprint
        let deviceFingerprint = deviceInfo.createDeviceFingerprint()
        
        // 3. Request registration challenge
        let challengeRequest = DeviceRegistrationChallengeRequest(
            deviceName: deviceInfo.deviceName,
            deviceType: "mobile",
            deviceFingerprint: deviceFingerprint,
            publicKey: String(data: publicKey, encoding: .utf8)!,
            keyAlgorithm: keyAlgorithm
        )
        
        let challengeResponse = try await apiClient.createDeviceRegistrationChallenge(challengeRequest)
        
        // 4. Sign the challenge
        let signature = try await keyManager.signChallenge(challengeResponse.challenge)
        
        // 5. Complete registration
        let verificationRequest = DeviceRegistrationVerifyRequest(
            sessionId: challengeResponse.sessionId,
            signedChallenge: signature
        )
        
        let verificationResponse = try await apiClient.verifyDeviceRegistration(verificationRequest)
        
        guard verificationResponse.success else {
            throw APIError.registrationFailed
        }
        
        // 6. Store device info locally
        try await DeviceStorage.shared.saveDevice(verificationResponse.device!)
        
        return verificationResponse.device!
    }
}

4. Biometric Login

BiometricLoginService.swift

class BiometricLoginService {
    private let apiClient: APIClient
    private let keyManager: BiometricKeyManager
    private let tokenStorage: TokenStorage
    
    func loginWithBiometrics(rememberMe: Bool = false) async throws -> TokenResponse {
        // 1. Get device fingerprint
        let deviceFingerprint = DeviceInfoProvider.shared.createDeviceFingerprint()
        
        // 2. Request login challenge
        let challengeRequest = MobileBiometricChallengeRequest(
            deviceFingerprint: deviceFingerprint
        )
        
        let challengeResponse = try await apiClient.createMobileBiometricChallenge(challengeRequest)
        
        // 3. Sign challenge with biometric authentication
        let signature = try await keyManager.signChallenge(challengeResponse.challenge)
        
        // 4. Complete login
        let loginRequest = MobileBiometricLoginRequest(
            sessionId: challengeResponse.sessionId,
            signedChallenge: signature,
            rememberMe: rememberMe
        )
        
        let loginResponse = try await apiClient.verifyMobileBiometricLogin(loginRequest)
        
        guard loginResponse.success, let tokens = loginResponse.tokens else {
            throw APIError.loginFailed
        }
        
        // 5. Store tokens securely
        try await tokenStorage.storeTokens(tokens)
        
        return tokens
    }
    
    func refreshTokens() async throws -> TokenResponse {
        guard let refreshToken = try await tokenStorage.getRefreshToken() else {
            throw APIError.noRefreshToken
        }
        
        let refreshRequest = MobileBiometricRefreshRequest(refreshToken: refreshToken)
        let newTokens = try await apiClient.refreshMobileBiometricToken(refreshRequest)
        
        try await tokenStorage.storeTokens(newTokens)
        return newTokens
    }
}

5. Device Info Provider

DeviceInfoProvider.swift

import UIKit
import LocalAuthentication

class DeviceInfoProvider {
    static let shared = DeviceInfoProvider()
    
    var deviceName: String {
        return UIDevice.current.name
    }
    
    func createDeviceFingerprint() -> String {
        let device = UIDevice.current
        let context = LAContext()
        
        // Get biometric type
        var biometricType = "Unknown"
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil) {
            switch context.biometryType {
            case .faceID:
                biometricType = "FaceID"
            case .touchID:
                biometricType = "TouchID"
            default:
                biometricType = "None"
            }
        }
        
        // Create unique fingerprint
        let components = [
            device.systemName,
            device.systemVersion,
            device.model,
            biometricType,
            getDeviceIdentifier()
        ]
        
        return components.joined(separator: "-")
    }
    
    private func getDeviceIdentifier() -> String {
        // Use a combination of identifierForVendor and keychain-stored UUID
        if let vendorId = UIDevice.current.identifierForVendor?.uuidString {
            return String(vendorId.prefix(8))
        }
        return "Unknown"
    }
}

Android Integration

Prerequisites

  • Android API Level 23+ (Android 6.0) for fingerprint
  • Android API Level 28+ (Android 9.0) for biometric prompt
  • Kotlin 1.8+
  • AndroidX Biometric library

1. Setup Dependencies

build.gradle (Module: app)

dependencies {
    implementation 'androidx.biometric:biometric:1.2.0-alpha05'
    implementation 'androidx.security:security-crypto:1.1.0-alpha06'
    
    // Firebase
    implementation 'com.google.firebase:firebase-messaging:23.4.0'
    
    // Networking
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
    implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
    
    // Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
    
    // Security
    implementation 'androidx.security:security-crypto:1.1.0-alpha06'
}

2. Biometric Key Management

BiometricKeyManager.kt

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import java.security.*
import java.security.spec.ECGenParameterSpec
import javax.crypto.Cipher

class BiometricKeyManager(private val activity: FragmentActivity) {
    private val keyAlias = "PVPipeBiometricKey"
    private val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
    
    sealed class BiometricResult {
        object Success : BiometricResult()
        object UserCancel : BiometricResult()
        object AuthenticationFailed : BiometricResult()
        data class Error(val message: String) : BiometricResult()
    }
    
    fun generateKeyPair(): Pair<String, String> {
        val keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore")
        
        val keyGenParameterSpec = KeyGenParameterSpec.Builder(
            keyAlias,
            KeyProperties.PURPOSE_SIGN
        )
            .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
            .setDigests(KeyProperties.DIGEST_SHA256)
            .setUserAuthenticationRequired(true)
            .setUserAuthenticationValidityDurationSeconds(30)
            .setInvalidatedByBiometricEnrollment(true)
            .build()
        
        keyPairGenerator.initialize(keyGenParameterSpec)
        val keyPair = keyPairGenerator.generateKeyPair()
        
        // Export public key in PEM format
        val publicKeyBytes = keyPair.public.encoded
        val pemKey = createPEMFromPublicKey(publicKeyBytes)
        
        return Pair(pemKey, "ES256")
    }
    
    suspend fun signChallenge(challenge: String): Result<String> = 
        suspendCancellableCoroutine { continuation ->
            try {
                val privateKey = keyStore.getKey(keyAlias, null) as PrivateKey
                val signature = Signature.getInstance("SHA256withECDSA")
                signature.initSign(privateKey)
                
                val biometricPrompt = BiometricPrompt(
                    activity as androidx.fragment.app.FragmentActivity,
                    ContextCompat.getMainExecutor(activity),
                    object : BiometricPrompt.AuthenticationCallback() {
                        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                            super.onAuthenticationSucceeded(result)
                            try {
                                signature.update(challenge.toByteArray())
                                val signatureBytes = signature.sign()
                                val base64Signature = Base64.encodeToString(signatureBytes, Base64.NO_WRAP)
                                continuation.resume(Result.success(base64Signature))
                            } catch (e: Exception) {
                                continuation.resume(Result.failure(e))
                            }
                        }
                        
                        override fun onAuthenticationFailed() {
                            super.onAuthenticationFailed()
                            continuation.resume(Result.failure(Exception("Authentication failed")))
                        }
                        
                        override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                            super.onAuthenticationError(errorCode, errString)
                            when (errorCode) {
                                BiometricPrompt.ERROR_USER_CANCELED -> {
                                    continuation.resume(Result.failure(Exception("User cancelled")))
                                }
                                else -> {
                                    continuation.resume(Result.failure(Exception(errString.toString())))
                                }
                            }
                        }
                    }
                )
                
                val promptInfo = BiometricPrompt.PromptInfo.Builder()
                    .setTitle("Sign in to PVPipe")
                    .setSubtitle("Use your biometric credential to sign the authentication challenge")
                    .setNegativeButtonText("Cancel")
                    .build()
                
                biometricPrompt.authenticate(promptInfo)
                
            } catch (e: Exception) {
                continuation.resume(Result.failure(e))
            }
        }
    
    private fun createPEMFromPublicKey(publicKeyBytes: ByteArray): String {
        val base64Key = Base64.encodeToString(publicKeyBytes, Base64.DEFAULT)
        return "-----BEGIN PUBLIC KEY-----\n$base64Key-----END PUBLIC KEY-----"
    }
    
    fun deleteKey(): Boolean {
        return try {
            keyStore.deleteEntry(keyAlias)
            true
        } catch (e: Exception) {
            false
        }
    }
    
    fun isKeyAvailable(): Boolean {
        return keyStore.containsAlias(keyAlias)
    }
}

3. Device Registration

DeviceRegistrationService.kt

class DeviceRegistrationService(
    private val apiClient: ApiClient,
    private val keyManager: BiometricKeyManager,
    private val deviceInfoProvider: DeviceInfoProvider
) {
    
    suspend fun registerDevice(): Result<RegisteredDevice> = withContext(Dispatchers.IO) {
        try {
            // 1. Generate key pair
            val (publicKey, keyAlgorithm) = keyManager.generateKeyPair()
            
            // 2. Create device fingerprint
            val deviceFingerprint = deviceInfoProvider.createDeviceFingerprint()
            
            // 3. Request registration challenge
            val challengeRequest = DeviceRegistrationChallengeRequest(
                deviceName = deviceInfoProvider.deviceName,
                deviceType = "mobile",
                deviceFingerprint = deviceFingerprint,
                publicKey = publicKey,
                keyAlgorithm = keyAlgorithm
            )
            
            val challengeResponse = apiClient.createDeviceRegistrationChallenge(challengeRequest)
            
            // 4. Sign the challenge
            val signatureResult = keyManager.signChallenge(challengeResponse.challenge)
            val signature = signatureResult.getOrThrow()
            
            // 5. Complete registration
            val verificationRequest = DeviceRegistrationVerifyRequest(
                sessionId = challengeResponse.sessionId,
                signedChallenge = signature
            )
            
            val verificationResponse = apiClient.verifyDeviceRegistration(verificationRequest)
            
            if (verificationResponse.success && verificationResponse.device != null) {
                // Store device info locally
                DeviceStorage.saveDevice(verificationResponse.device)
                Result.success(verificationResponse.device)
            } else {
                Result.failure(Exception("Registration failed"))
            }
            
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

4. Device Info Provider

DeviceInfoProvider.kt

import android.content.Context
import android.os.Build
import androidx.biometric.BiometricManager

class DeviceInfoProvider(private val context: Context) {
    
    val deviceName: String
        get() = "${Build.MANUFACTURER} ${Build.MODEL}"
    
    fun createDeviceFingerprint(): String {
        val biometricManager = BiometricManager.from(context)
        
        val biometricType = when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
            BiometricManager.BIOMETRIC_SUCCESS -> "Biometric"
            else -> "None"
        }
        
        val components = listOf(
            "Android",
            Build.VERSION.RELEASE,
            Build.MODEL,
            biometricType,
            getDeviceIdentifier()
        )
        
        return components.joinToString("-")
    }
    
    private fun getDeviceIdentifier(): String {
        // Create a stable device identifier
        return Build.FINGERPRINT.hashCode().toString().take(8)
    }
}

React Native Integration

Prerequisites

  • React Native 0.72+
  • iOS 12.0+ / Android API 23+
  • React Native Biometrics library

1. Installation

npm install react-native-biometrics
npm install @react-native-firebase/messaging
npm install @react-native-async-storage/async-storage
npm install react-native-keychain

# iOS specific
cd ios && pod install

2. Biometric Service

BiometricService.js

import ReactNativeBiometrics from 'react-native-biometrics';
import AsyncStorage from '@react-native-async-storage/async-storage';
import Keychain from 'react-native-keychain';

class BiometricService {
  constructor() {
    this.rnBiometrics = new ReactNativeBiometrics();
  }

  async isSupported() {
    try {
      const { available, biometryType } = await this.rnBiometrics.isSensorAvailable();
      return { available, biometryType };
    } catch (error) {
      return { available: false, biometryType: null };
    }
  }

  async generateKeyPair() {
    try {
      const { keysExist } = await this.rnBiometrics.biometricKeysExist();
      
      if (keysExist) {
        await this.rnBiometrics.deleteKeys();
      }

      const { publicKey } = await this.rnBiometrics.createKeys();
      
      // Convert to PEM format
      const pemKey = this.convertToPEM(publicKey);
      
      return {
        publicKey: pemKey,
        keyAlgorithm: 'ES256'
      };
    } catch (error) {
      throw new Error(`Key generation failed: ${error.message}`);
    }
  }

  async signChallenge(challenge) {
    try {
      const { success, signature } = await this.rnBiometrics.createSignature({
        promptMessage: 'Sign in to PVPipe',
        payload: challenge
      });

      if (!success) {
        throw new Error('Biometric authentication failed');
      }

      return signature;
    } catch (error) {
      if (error.message.includes('User cancel')) {
        throw new Error('USER_CANCEL');
      }
      throw error;
    }
  }

  convertToPEM(publicKey) {
    const header = '-----BEGIN PUBLIC KEY-----\n';
    const footer = '\n-----END PUBLIC KEY-----';
    const formattedKey = publicKey.match(/.{1,64}/g).join('\n');
    return header + formattedKey + footer;
  }

  async deleteKeys() {
    try {
      await this.rnBiometrics.deleteKeys();
      return true;
    } catch (error) {
      return false;
    }
  }
}

export default new BiometricService();

3. Device Registration Hook

useDeviceRegistration.js

import { useState, useCallback } from 'react';
import BiometricService from '../services/BiometricService';
import ApiClient from '../services/ApiClient';
import DeviceInfo from 'react-native-device-info';

export const useDeviceRegistration = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const createDeviceFingerprint = useCallback(async () => {
    const deviceName = await DeviceInfo.getDeviceName();
    const systemName = DeviceInfo.getSystemName();
    const systemVersion = DeviceInfo.getSystemVersion();
    const model = DeviceInfo.getModel();
    const uniqueId = DeviceInfo.getUniqueId();

    const { biometryType } = await BiometricService.isSupported();
    
    return `${systemName}-${systemVersion}-${model}-${biometryType || 'None'}-${uniqueId.slice(0, 8)}`;
  }, []);

  const registerDevice = useCallback(async () => {
    setIsLoading(true);
    setError(null);

    try {
      // Check biometric availability
      const { available } = await BiometricService.isSupported();
      if (!available) {
        throw new Error('Biometric authentication not available');
      }

      // Generate key pair
      const { publicKey, keyAlgorithm } = await BiometricService.generateKeyPair();

      // Create device fingerprint
      const deviceFingerprint = await createDeviceFingerprint();
      const deviceName = await DeviceInfo.getDeviceName();

      // Request registration challenge
      const challengeResponse = await ApiClient.createDeviceRegistrationChallenge({
        deviceName,
        deviceType: 'mobile',
        deviceFingerprint,
        publicKey,
        keyAlgorithm
      });

      // Sign challenge
      const signature = await BiometricService.signChallenge(challengeResponse.challenge);

      // Complete registration
      const verificationResponse = await ApiClient.verifyDeviceRegistration({
        sessionId: challengeResponse.sessionId,
        signedChallenge: signature
      });

      if (verificationResponse.success) {
        // Store device info
        await AsyncStorage.setItem('registeredDevice', JSON.stringify(verificationResponse.device));
        return verificationResponse.device;
      } else {
        throw new Error('Device registration failed');
      }

    } catch (err) {
      setError(err.message);
      throw err;
    } finally {
      setIsLoading(false);
    }
  }, [createDeviceFingerprint]);

  return {
    registerDevice,
    isLoading,
    error
  };
};

4. Biometric Login Hook

useBiometricLogin.js

import { useState, useCallback } from 'react';
import BiometricService from '../services/BiometricService';
import ApiClient from '../services/ApiClient';
import TokenStorage from '../services/TokenStorage';

export const useBiometricLogin = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState(null);

  const login = useCallback(async (rememberMe = false) => {
    setIsLoading(true);
    setError(null);

    try {
      // Get device fingerprint
      const deviceFingerprint = await createDeviceFingerprint();

      // Request login challenge
      const challengeResponse = await ApiClient.createMobileBiometricChallenge({
        deviceFingerprint
      });

      // Sign challenge with biometric
      const signature = await BiometricService.signChallenge(challengeResponse.challenge);

      // Complete login
      const loginResponse = await ApiClient.verifyMobileBiometricLogin({
        sessionId: challengeResponse.sessionId,
        signedChallenge: signature,
        rememberMe
      });

      if (loginResponse.success && loginResponse.tokens) {
        // Store tokens securely
        await TokenStorage.storeTokens(loginResponse.tokens);
        return loginResponse.tokens;
      } else {
        throw new Error('Login failed');
      }

    } catch (err) {
      if (err.message === 'USER_CANCEL') {
        setError('Authentication cancelled by user');
      } else {
        setError(err.message);
      }
      throw err;
    } finally {
      setIsLoading(false);
    }
  }, []);

  const refreshTokens = useCallback(async () => {
    try {
      const refreshToken = await TokenStorage.getRefreshToken();
      if (!refreshToken) {
        throw new Error('No refresh token available');
      }

      const newTokens = await ApiClient.refreshMobileBiometricToken({
        refreshToken
      });

      await TokenStorage.storeTokens(newTokens);
      return newTokens;
    } catch (error) {
      await TokenStorage.clearTokens();
      throw error;
    }
  }, []);

  return {
    login,
    refreshTokens,
    isLoading,
    error
  };
};

Security Best Practices

1. Key Storage Security

iOS Best Practices

// Use Secure Enclave when available
let accessControl = SecAccessControlCreateWithFlags(
    kCFAllocatorDefault,
    kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
    [.privateKeyUsage, .biometryAny, .applicationPassword],
    nil
)

// Additional security attributes
let keyAttributes: [String: Any] = [
    kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave, // Secure Enclave
    kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
    kSecAttrKeySizeInBits as String: 256
]

Android Best Practices

// Use hardware-backed keystore
val keyGenParameterSpec = KeyGenParameterSpec.Builder(keyAlias, KeyProperties.PURPOSE_SIGN)
    .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
    .setDigests(KeyProperties.DIGEST_SHA256)
    .setUserAuthenticationRequired(true)
    .setUserAuthenticationValidityDurationSeconds(30)
    .setIsStrongBoxBacked(true) // Hardware security module
    .setInvalidatedByBiometricEnrollment(true)
    .build()

2. Network Security

Certificate Pinning

// React Native - Certificate pinning
const ApiClient = {
  baseURL: 'https://auth.pvpipe.com',
  
  async request(endpoint, options = {}) {
    const config = {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      }
    };

    // Certificate pinning validation
    if (__DEV__) {
      // Development - allow self-signed certificates
      config.trustAllCerts = true;
    } else {
      // Production - strict certificate validation
      config.sslPinning = {
        certs: ['auth.pvpipe.com.crt']
      };
    }

    return fetch(`${this.baseURL}${endpoint}`, config);
  }
};

3. Token Security

Secure Token Storage

// TokenStorage.js
import Keychain from 'react-native-keychain';
import AsyncStorage from '@react-native-async-storage/async-storage';

class TokenStorage {
  static async storeTokens(tokens) {
    try {
      // Store access token in memory only (short-lived)
      this.accessToken = tokens.accessToken;
      
      // Store refresh token in keychain (long-lived)
      await Keychain.setItem('refreshToken', tokens.refreshToken, {
        accessGroup: 'com.pvpipe.tokens',
        accessibleWhenUnlocked: true,
        authenticationType: Keychain.AUTHENTICATION_TYPE.BIOMETRICS
      });
      
      // Store token metadata
      await AsyncStorage.setItem('tokenMetadata', JSON.stringify({
        accessTokenExpiresAt: tokens.accessTokenExpiresAt,
        refreshTokenExpiresAt: tokens.refreshTokenExpiresAt
      }));
      
    } catch (error) {
      throw new Error('Failed to store tokens securely');
    }
  }

  static async getAccessToken() {
    // Check if access token is still valid
    const metadata = await this.getTokenMetadata();
    if (metadata && new Date() < new Date(metadata.accessTokenExpiresAt)) {
      return this.accessToken;
    }
    
    // Auto-refresh if needed
    await this.refreshTokens();
    return this.accessToken;
  }

  static async clearTokens() {
    this.accessToken = null;
    await Keychain.removeItem('refreshToken');
    await AsyncStorage.removeItem('tokenMetadata');
  }
}

4. Anti-Tampering Measures

iOS Jailbreak Detection

class SecurityValidator {
    static func isDeviceSecure() -> Bool {
        return !isJailbroken() && !isDebugging()
    }
    
    private static func isJailbroken() -> Bool {
        let jailbreakPaths = [
            "/Applications/Cydia.app",
            "/bin/bash",
            "/usr/sbin/sshd",
            "/etc/apt"
        ]
        
        for path in jailbreakPaths {
            if FileManager.default.fileExists(atPath: path) {
                return true
            }
        }
        
        return false
    }
    
    private static func isDebugging() -> Bool {
        var info = kinfo_proc()
        var mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()]
        var size = MemoryLayout.stride(ofValue: info)
        
        let junk = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0)
        assert(junk == 0, "sysctl failed")
        
        return (info.kp_proc.p_flag & P_TRACED) != 0
    }
}

Android Root Detection

class SecurityValidator {
    fun isDeviceSecure(): Boolean {
        return !isRooted() && !isDebugging()
    }
    
    private fun isRooted(): Boolean {
        val rootPaths = arrayOf(
            "/system/app/Superuser.apk",
            "/sbin/su",
            "/system/bin/su",
            "/system/xbin/su",
            "/data/local/xbin/su",
            "/data/local/bin/su",
            "/system/sd/xbin/su",
            "/system/bin/failsafe/su",
            "/data/local/su"
        )
        
        for (path in rootPaths) {
            if (File(path).exists()) {
                return true
            }
        }
        
        return false
    }
    
    private fun isDebugging(): Boolean {
        return (applicationContext.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
    }
}

Push Notifications

1. Firebase Setup

iOS Configuration

// AppDelegate.swift
import Firebase
import FirebaseMessaging

class AppDelegate: UIResponder, UIApplicationDelegate {
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        FirebaseApp.configure()
        
        // Set messaging delegate
        Messaging.messaging().delegate = self
        
        // Register for remote notifications
        UNUserNotificationCenter.current().delegate = self
        
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { granted, error in
            if granted {
                DispatchQueue.main.async {
                    application.registerForRemoteNotifications()
                }
            }
        }
        
        return true
    }
}

extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        // Update FCM token with server
        if let token = fcmToken {
            updateFCMToken(token)
        }
    }
    
    private func updateFCMToken(_ token: String) {
        Task {
            do {
                await ApiClient.shared.updateDeviceFCMToken(token: token)
            } catch {
                print("Failed to update FCM token: \(error)")
            }
        }
    }
}

2. Notification Handling

React Native Push Notifications

// NotificationService.js
import messaging from '@react-native-firebase/messaging';
import AsyncStorage from '@react-native-async-storage/async-storage';

class NotificationService {
  static async initialize() {
    // Request permission
    const authStatus = await messaging().requestPermission();
    const enabled = authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
                   authStatus === messaging.AuthorizationStatus.PROVISIONAL;

    if (enabled) {
      // Get FCM token
      const token = await messaging().getToken();
      await this.updateFCMToken(token);
      
      // Listen for token refresh
      messaging().onTokenRefresh(token => {
        this.updateFCMToken(token);
      });
      
      // Handle foreground messages
      messaging().onMessage(async remoteMessage => {
        this.handleNotification(remoteMessage);
      });
      
      // Handle background messages
      messaging().setBackgroundMessageHandler(async remoteMessage => {
        this.handleNotification(remoteMessage);
      });
    }
  }

  static async updateFCMToken(token) {
    try {
      const device = await AsyncStorage.getItem('registeredDevice');
      if (device) {
        const deviceInfo = JSON.parse(device);
        await ApiClient.updateDeviceFCMToken({
          deviceId: deviceInfo.id,
          fcmToken: token
        });
      }
    } catch (error) {
      console.error('Failed to update FCM token:', error);
    }
  }

  static handleNotification(remoteMessage) {
    if (remoteMessage.data?.type === 'confirmation_request') {
      // Show confirmation dialog
      this.showConfirmationDialog(remoteMessage.data);
    } else if (remoteMessage.data?.type === 'security_alert') {
      // Show security alert
      this.showSecurityAlert(remoteMessage.data);
    }
  }

  static showConfirmationDialog(data) {
    // Navigate to confirmation screen or show modal
    const confirmationId = data.confirmationId;
    const actionType = data.actionType;
    
    // Implementation depends on your navigation library
    Navigation.push('ConfirmationScreen', {
      confirmationId,
      actionType,
      actionPayload: JSON.parse(data.actionPayload || '{}')
    });
  }
}

export default NotificationService;

Error Handling

1. Comprehensive Error Types

// ErrorTypes.js
export const BiometricErrorTypes = {
  // Hardware/System Errors
  BIOMETRIC_NOT_AVAILABLE: 'BIOMETRIC_NOT_AVAILABLE',
  BIOMETRIC_NOT_ENROLLED: 'BIOMETRIC_NOT_ENROLLED',
  HARDWARE_NOT_AVAILABLE: 'HARDWARE_NOT_AVAILABLE',
  
  // Authentication Errors
  USER_CANCEL: 'USER_CANCEL',
  AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED',
  TOO_MANY_ATTEMPTS: 'TOO_MANY_ATTEMPTS',
  
  // Key Management Errors
  KEY_GENERATION_FAILED: 'KEY_GENERATION_FAILED',
  KEY_NOT_FOUND: 'KEY_NOT_FOUND',
  SIGNATURE_FAILED: 'SIGNATURE_FAILED',
  
  // Network Errors
  NETWORK_ERROR: 'NETWORK_ERROR',
  SERVER_ERROR: 'SERVER_ERROR',
  DEVICE_NOT_REGISTERED: 'DEVICE_NOT_REGISTERED',
  INVALID_CHALLENGE: 'INVALID_CHALLENGE',
  
  // Token Errors
  TOKEN_EXPIRED: 'TOKEN_EXPIRED',
  TOKEN_INVALID: 'TOKEN_INVALID',
  REFRESH_FAILED: 'REFRESH_FAILED'
};

export class BiometricError extends Error {
  constructor(type, message, originalError = null) {
    super(message);
    this.type = type;
    this.originalError = originalError;
    this.name = 'BiometricError';
  }
}

2. Error Handler Service

// ErrorHandler.js
import { BiometricError, BiometricErrorTypes } from './ErrorTypes';

class ErrorHandler {
  static handle(error) {
    console.error('Biometric Error:', error);
    
    if (error instanceof BiometricError) {
      return this.handleBiometricError(error);
    }
    
    // Convert generic errors to BiometricError
    return this.convertError(error);
  }
  
  static handleBiometricError(error) {
    switch (error.type) {
      case BiometricErrorTypes.BIOMETRIC_NOT_AVAILABLE:
        return {
          title: 'Biometric Authentication Unavailable',
          message: 'Your device does not support biometric authentication.',
          action: 'USE_PASSWORD'
        };
        
      case BiometricErrorTypes.BIOMETRIC_NOT_ENROLLED:
        return {
          title: 'Setup Required',
          message: 'Please set up biometric authentication in your device settings.',
          action: 'OPEN_SETTINGS'
        };
        
      case BiometricErrorTypes.USER_CANCEL:
        return {
          title: 'Authentication Cancelled',
          message: 'Biometric authentication was cancelled.',
          action: 'RETRY'
        };
        
      case BiometricErrorTypes.DEVICE_NOT_REGISTERED:
        return {
          title: 'Device Not Registered',
          message: 'This device is not registered for biometric authentication.',
          action: 'REGISTER_DEVICE'
        };
        
      case BiometricErrorTypes.TOKEN_EXPIRED:
        return {
          title: 'Session Expired',
          message: 'Your session has expired. Please sign in again.',
          action: 'SIGN_IN'
        };
        
      default:
        return {
          title: 'Authentication Error',
          message: error.message || 'An unexpected error occurred.',
          action: 'RETRY'
        };
    }
  }
  
  static convertError(error) {
    if (error.message?.includes('User cancel')) {
      return new BiometricError(
        BiometricErrorTypes.USER_CANCEL,
        'Authentication cancelled by user',
        error
      );
    }
    
    if (error.message?.includes('Network')) {
      return new BiometricError(
        BiometricErrorTypes.NETWORK_ERROR,
        'Network connection failed',
        error
      );
    }
    
    return new BiometricError(
      'UNKNOWN_ERROR',
      error.message || 'An unexpected error occurred',
      error
    );
  }
}

export default ErrorHandler;

Testing Guide

1. Unit Testing

iOS Tests

// BiometricKeyManagerTests.swift
import XCTest
@testable import PVPipeApp

class BiometricKeyManagerTests: XCTestCase {
    var keyManager: BiometricKeyManager!
    
    override func setUp() {
        super.setUp()
        keyManager = BiometricKeyManager()
    }
    
    override func tearDown() {
        keyManager?.deleteKey()
        super.tearDown()
    }
    
    func testKeyGeneration() async {
        do {
            let (publicKey, algorithm) = try await keyManager.generateKeyPair()
            
            XCTAssertFalse(publicKey.isEmpty)
            XCTAssertEqual(algorithm, "ES256")
            XCTAssertTrue(String(data: publicKey, encoding: .utf8)!.contains("BEGIN PUBLIC KEY"))
        } catch {
            XCTFail("Key generation failed: \(error)")
        }
    }
    
    func testSignChallenge() async {
        // First generate a key pair
        _ = try! await keyManager.generateKeyPair()
        
        let challenge = "test-challenge-12345"
        
        do {
            let signature = try await keyManager.signChallenge(challenge)
            XCTAssertFalse(signature.isEmpty)
            
            // Verify signature format (base64)
            let signatureData = Data(base64Encoded: signature)
            XCTAssertNotNil(signatureData)
        } catch {
            XCTFail("Challenge signing failed: \(error)")
        }
    }
}

Android Tests

// BiometricKeyManagerTest.kt
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*

@RunWith(AndroidJUnit4::class)
class BiometricKeyManagerTest {
    
    private val context = InstrumentationRegistry.getInstrumentation().targetContext
    private val keyManager = BiometricKeyManager(context as FragmentActivity)
    
    @Test
    fun testKeyGeneration() {
        val (publicKey, algorithm) = keyManager.generateKeyPair()
        
        assertFalse(publicKey.isEmpty())
        assertEquals("ES256", algorithm)
        assertTrue(publicKey.contains("BEGIN PUBLIC KEY"))
    }
    
    @Test
    fun testKeyDeletion() {
        // Generate key first
        keyManager.generateKeyPair()
        
        // Verify key exists
        assertTrue(keyManager.isKeyAvailable())
        
        // Delete key
        assertTrue(keyManager.deleteKey())
        
        // Verify key is deleted
        assertFalse(keyManager.isKeyAvailable())
    }
}

2. Integration Testing

React Native Integration Tests

// __tests__/BiometricIntegration.test.js
import BiometricService from '../src/services/BiometricService';
import ApiClient from '../src/services/ApiClient';

// Mock the native modules
jest.mock('react-native-biometrics', () => ({
  isSensorAvailable: jest.fn(),
  createKeys: jest.fn(),
  createSignature: jest.fn(),
  deleteKeys: jest.fn()
}));

describe('Biometric Integration Tests', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });
  
  test('should complete device registration flow', async () => {
    // Mock biometric availability
    BiometricService.isSupported = jest.fn().mockResolvedValue({
      available: true,
      biometryType: 'TouchID'
    });
    
    // Mock key generation
    BiometricService.generateKeyPair = jest.fn().mockResolvedValue({
      publicKey: '-----BEGIN PUBLIC KEY-----\nMOCK_KEY\n-----END PUBLIC KEY-----',
      keyAlgorithm: 'ES256'
    });
    
    // Mock API responses
    ApiClient.createDeviceRegistrationChallenge = jest.fn().mockResolvedValue({
      challenge: 'mock-challenge',
      sessionId: 'mock-session-id',
      deviceId: 'mock-device-id',
      expiresAt: new Date(Date.now() + 300000).toISOString()
    });
    
    BiometricService.signChallenge = jest.fn().mockResolvedValue('mock-signature');
    
    ApiClient.verifyDeviceRegistration = jest.fn().mockResolvedValue({
      success: true,
      deviceId: 'mock-device-id',
      device: {
        id: 'mock-device-id',
        deviceName: 'Test Device',
        isActive: true
      }
    });
    
    // Test the registration flow
    const { registerDevice } = useDeviceRegistration();
    const result = await registerDevice();
    
    expect(result.success).toBe(true);
    expect(BiometricService.generateKeyPair).toHaveBeenCalled();
    expect(ApiClient.createDeviceRegistrationChallenge).toHaveBeenCalled();
    expect(BiometricService.signChallenge).toHaveBeenCalledWith('mock-challenge');
    expect(ApiClient.verifyDeviceRegistration).toHaveBeenCalled();
  });
  
  test('should handle biometric authentication flow', async () => {
    // Mock successful login flow
    ApiClient.createMobileBiometricChallenge = jest.fn().mockResolvedValue({
      challenge: 'login-challenge',
      sessionId: 'login-session-id',
      expiresAt: new Date(Date.now() + 120000).toISOString()
    });
    
    BiometricService.signChallenge = jest.fn().mockResolvedValue('login-signature');
    
    ApiClient.verifyMobileBiometricLogin = jest.fn().mockResolvedValue({
      success: true,
      tokens: {
        accessToken: 'mock-access-token',
        refreshToken: 'mock-refresh-token',
        accessTokenExpiresAt: new Date(Date.now() + 900000).toISOString(),
        refreshTokenExpiresAt: new Date(Date.now() + 2592000000).toISOString()
      }
    });
    
    const { login } = useBiometricLogin();
    const tokens = await login(true);
    
    expect(tokens.accessToken).toBe('mock-access-token');
    expect(tokens.refreshToken).toBe('mock-refresh-token');
  });
});

3. End-to-End Testing

Detox E2E Tests (React Native)

// e2e/biometric.e2e.js
describe('Biometric Authentication E2E', () => {
  beforeEach(async () => {
    await device.reloadReactNative();
  });
  
  it('should register device successfully', async () => {
    // Navigate to device registration
    await element(by.id('register-device-button')).tap();
    
    // Fill device name
    await element(by.id('device-name-input')).typeText('Test Device');
    
    // Start registration
    await element(by.id('start-registration-button')).tap();
    
    // Simulate biometric authentication
    await device.simulateBiometric('success');
    
    // Verify success screen
    await expect(element(by.id('registration-success'))).toBeVisible();
  });
  
  it('should login with biometrics', async () => {
    // Assuming device is already registered
    await element(by.id('biometric-login-button')).tap();
    
    // Simulate biometric authentication
    await device.simulateBiometric('success');
    
    // Verify logged in state
    await expect(element(by.id('dashboard'))).toBeVisible();
  });
  
  it('should handle biometric authentication failure', async () => {
    await element(by.id('biometric-login-button')).tap();
    
    // Simulate biometric failure
    await device.simulateBiometric('failure');
    
    // Verify error message
    await expect(element(by.text('Authentication failed'))).toBeVisible();
  });
});

This comprehensive mobile integration guide provides everything needed to implement biometric authentication in iOS, Android, and React Native applications with the PVPipe system, including security best practices, error handling, and testing strategies.