ios_biometric_auth
Enable biometric authentication for Lightning Network operations on iOS devices. Use Touch ID or Face ID to authorize payments, channel management, and seed export.
Instructions
Integrate Touch/Face ID with Lightning operations
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| operation | No | Operation requiring biometric auth | send_payment |
| requireAuth | No | Whether to require biometric auth |
Implementation Reference
- src/tools/iosBiometricAuth.ts:25-181 (handler)The main handler (execute function) for the ios_biometric_auth tool. It processes input arguments, calls the iosService.testBiometricAuth() helper, and returns a ToolResult with success status, security guidelines, and comprehensive SwiftUI example code for biometric-protected payments.execute: async (args: any): Promise<ToolResult> => { try { const result = await iosService.testBiometricAuth(); return { content: [{ type: 'text', text: JSON.stringify({ success: result.success, message: result.message, swiftExample: result.swiftCode, operation: args.operation, requireAuth: args.requireAuth, securityGuidelines: [ 'Always require biometric auth for seed access', 'Use biometrics for high-value payments', 'Provide passcode fallback option', 'Clear biometric data on app reinstall', 'Implement anti-tampering measures', 'Use LAContext evaluation for each operation' ], uiIntegration: ` // SwiftUI biometric-protected payment view import SwiftUI import LocalAuthentication struct SecurePaymentView: View { @State private var isAuthenticated = false @State private var showingError = false @State private var errorMessage = "" let invoice: String let amountSats: Int var body: some View { VStack(spacing: 30) { // Payment details VStack(spacing: 16) { Image(systemName: "bolt.circle.fill") .font(.system(size: 60)) .foregroundColor(.orange) Text("Confirm Payment") .font(.title) .fontWeight(.semibold) Text("\\(amountSats) sats") .font(.title2) .foregroundColor(.secondary) } // Biometric prompt if !isAuthenticated { VStack(spacing: 20) { Image(systemName: biometricIcon) .font(.system(size: 50)) .foregroundColor(.blue) Text("Authenticate to send payment") .font(.headline) Button(action: authenticate) { Label("Authenticate", systemImage: biometricIcon) .frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent) .controlSize(.large) } } else { // Payment in progress VStack(spacing: 16) { ProgressView() .scaleEffect(1.5) Text("Sending payment...") .font(.headline) } .onAppear { sendPayment() } } } .padding() .alert("Authentication Failed", isPresented: $showingError) { Button("OK") { } } message: { Text(errorMessage) } } var biometricIcon: String { let biometricType = BiometricLightningAuth.getBiometricType() switch biometricType { case .faceID: return "faceid" case .touchID: return "touchid" default: return "lock" } } func authenticate() { Task { let authenticated = await BiometricLightningAuth.authenticateForPayment( amount: UInt64(amountSats) ) if authenticated { withAnimation { isAuthenticated = true } } else { errorMessage = "Authentication failed. Please try again." showingError = true } } } func sendPayment() { Task { do { // Send payment via LDK try await LDKManager.shared.payInvoice( invoice: invoice, maxFeeSats: UInt64(amountSats) / 100 // 1% max fee ) // Navigate to success view await MainActor.run { // Show success } } catch { await MainActor.run { errorMessage = "Payment failed: \\(error.localizedDescription)" showingError = true } } } } }`.trim() }, null, 2) }] }; } catch (error) { return { content: [{ type: 'text', text: JSON.stringify({ success: false, error: error instanceof Error ? error.message : 'Unknown error' }, null, 2) }], isError: true }; } }
- src/tools/iosBiometricAuth.ts:9-24 (schema)The inputSchema defining the parameters for the ios_biometric_auth tool: operation (enum: send_payment, etc.) and requireAuth (boolean).inputSchema: { type: 'object', properties: { operation: { type: 'string', enum: ['send_payment', 'open_channel', 'close_channel', 'export_seed'], description: 'Operation requiring biometric auth', default: 'send_payment' }, requireAuth: { type: 'boolean', description: 'Whether to require biometric auth', default: true } } },
- src/index.ts:38-62 (registration)Registration of the biometricAuthTool (imported line 21, included line 47) in the central tools array used by MCP server request handlers for listTools and callTool.const tools = [ generateInvoiceTool, payInvoiceTool, getChannelStatusTool, getNodeInfoTool, backupStateTool, keychainTestTool, backgroundTestTool, pushNotificationTool, biometricAuthTool, createChannelTool, closeChannelTool, getBalanceTool, decodeInvoiceTool, listPaymentsTool, estimateFeeTool, generateMnemonicTool, deriveAddressTool, getSwiftCodeTool, getArchitectureTool, testScenarioTool, networkGraphTool, eventHandlingTool, chainSyncTool, ];
- src/services/iosService.ts:208-317 (helper)Supporting helper method testBiometricAuth() in IOSService class, which returns mock success response with detailed Swift code examples for biometric authentication using LocalAuthentication and biometric-protected Keychain access.async testBiometricAuth(): Promise<{ success: boolean; message: string; swiftCode: string; }> { const swiftCode = ` import LocalAuthentication import Security class BiometricLightningAuth { enum BiometricType { case none case touchID case faceID } static func getBiometricType() -> BiometricType { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { return .none } switch context.biometryType { case .touchID: return .touchID case .faceID: return .faceID default: return .none } } static func authenticateForPayment(amount: UInt64) async -> Bool { let context = LAContext() context.localizedCancelTitle = "Cancel" let reason = "Authenticate to send \\(amount / 1000) sats" do { let success = try await context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: reason ) return success } catch { print("Biometric authentication failed: \\(error)") return false } } static func protectSeedWithBiometrics(seed: Data) throws -> Data { let context = LAContext() context.localizedReason = "Protect your Lightning wallet" let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: "ldk_protected_seed", kSecValueData as String: seed, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly, kSecAttrAccessControl as String: SecAccessControlCreateWithFlags( nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, nil )! ] // Remove existing item SecItemDelete(query as CFDictionary) // Add with biometric protection let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecSuccess else { throw NSError(domain: "BiometricAuth", code: Int(status)) } return seed } static func retrieveSeedWithBiometrics() async throws -> Data { let context = LAContext() context.localizedReason = "Access your Lightning wallet" let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: "ldk_protected_seed", kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne, kSecUseAuthenticationContext as String: context ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess, let data = result as? Data else { throw NSError(domain: "BiometricAuth", code: Int(status)) } return data } }`.trim(); return { success: true, message: 'Biometric authentication test completed', swiftCode }; }