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
- Architecture Overview
- iOS Integration
- Android Integration
- React Native Integration
- Security Best Practices
- Push Notifications
- Error Handling
- 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
- Secure Key Storage: Hardware Security Module (HSM) or Secure Enclave
- Biometric Authentication: TouchID, FaceID, Fingerprint, etc.
- Challenge-Response: Cryptographic proof of device ownership
- Token Management: Secure storage and automatic refresh
- 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'
endPackage.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 install2. 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.