Przejdź do głównej zawartości

Biometria & PIN

Vista wspiera natywną biometrię na macOS przez LocalAuthentication framework.

PlatformBiometriaWymagania
macOSTouch IDMacBook Pro/Air z Touch ID, macOS 10.12+
macOSFace IDZewnętrzna kamera (przyszłość)
WindowsWindows HelloWindows 10+, kompatybilny sprzęt
Linux-Brak wsparcia

sequenceDiagram
participant U as User
participant FE as Frontend
participant Bio as useBiometric Hook
participant BE as Tauri Backend
participant LA as LocalAuthentication
participant DB as SQLite
U->>FE: Click "Login with Touch ID"
FE->>Bio: loginWithBiometric()
Bio->>BE: invoke('authenticate_biometric')
BE->>LA: evaluate_policy(DeviceOwnerAuthentication)
LA->>U: 🔐 Touch ID prompt
alt Success
LA-->>BE: Authentication successful
BE->>DB: Get user by biometric_key_id
BE->>DB: Create session
BE-->>FE: LoginResponse
FE-->>U: Logged in!
else Failure
LA-->>BE: Error (cancelled/failed)
BE-->>FE: Biometric auth failed
FE-->>U: Show password login
end

src-tauri/src/commands/biometrics.rs
use security_framework::os::macos::keychain::SecKeychain;
#[tauri::command]
pub async fn authenticate_biometric(
reason: String,
) -> Result<BiometricAuthResult, String> {
let context = LAContext::new();
// Check availability
if !context.can_evaluate_policy(LAPolicy::DeviceOwnerAuthenticationWithBiometrics) {
return Err("Biometric authentication not available".to_string());
}
// Perform authentication
let result = context.evaluate_policy(
LAPolicy::DeviceOwnerAuthenticationWithBiometrics,
&reason
).await;
match result {
Ok(_) => Ok(BiometricAuthResult::Success),
Err(e) => Ok(BiometricAuthResult::Failed(e.to_string()))
}
}
#[tauri::command]
pub async fn check_biometric_availability() -> Result<BiometricStatus, String> {
let context = LAContext::new();
let can_use_biometrics = context.can_evaluate_policy(
LAPolicy::DeviceOwnerAuthenticationWithBiometrics
);
let biometric_type = if can_use_biometrics {
context.biometry_type()
} else {
BiometryType::None
};
Ok(BiometricStatus {
available: can_use_biometrics,
biometric_type: biometric_type.into(),
})
}

src/hooks/auth/useBiometric.ts
export const useBiometric = () => {
const [isAvailable, setIsAvailable] = useState(false);
const [isEnabled, setIsEnabled] = useState(false);
const [biometricType, setBiometricType] = useState<'touchId' | 'faceId' | null>(null);
useEffect(() => {
checkAvailability();
}, []);
const checkAvailability = async () => {
const status = await invoke<BiometricStatus>('check_biometric_availability');
setIsAvailable(status.available);
setBiometricType(status.biometric_type);
};
const authenticate = async (reason: string): Promise<BiometricResult> => {
if (!isAvailable) {
return { success: false, error: 'Biometrics not available' };
}
try {
const result = await invoke<BiometricAuthResult>('authenticate_biometric', {
reason
});
return { success: result === 'Success', error: null };
} catch (error) {
return { success: false, error: String(error) };
}
};
const enable = async (userId: string): Promise<void> => {
// Authenticate first
const authResult = await authenticate('Enable biometric login for Vista');
if (!authResult.success) {
throw new Error('Biometric authentication required');
}
// Store biometric key in database
await invoke('enable_biometric_login', { userId });
setIsEnabled(true);
};
const disable = async (userId: string): Promise<void> => {
await invoke('disable_biometric_login', { userId });
setIsEnabled(false);
};
return {
isAvailable,
isEnabled,
biometricType,
authenticate,
enable,
disable,
};
};

-- Biometric enabled flag in users table
ALTER TABLE users ADD COLUMN biometric_enabled INTEGER DEFAULT 0;
ALTER TABLE users ADD COLUMN biometric_key_id TEXT;
-- PIN lockout tracking
CREATE TABLE pin_lockout (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,
failed_attempts INTEGER DEFAULT 0,
locked_until TIMESTAMP,
last_attempt TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

sequenceDiagram
participant U as User
participant FE as Settings
participant BE as Backend
participant DB as SQLite
U->>FE: Setup PIN
FE->>FE: Validate PIN (4-6 digits)
FE->>BE: set_pin(userId, pin)
BE->>BE: bcrypt.hash(pin)
BE->>DB: UPDATE users SET pin_hash = ?
BE-->>FE: Success
FE-->>U: PIN configured!
interface PinLockoutConfig {
maxAttempts: 5, // Lock after 5 failed attempts
lockoutDuration: 300, // 5 minutes
resetAfterSuccess: true, // Reset counter on success
}
-- Check lockout status
SELECT
failed_attempts,
locked_until,
CASE
WHEN locked_until > CURRENT_TIMESTAMP THEN 'locked'
ELSE 'unlocked'
END as status
FROM pin_lockout
WHERE user_id = ?;

┌─────────────────────────────────────────────────────────────┐
│ SECURITY BOUNDARIES │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ Biometric data NEVER leaves Secure Enclave │
│ ✅ Vista only receives success/failure result │
│ ✅ No biometric templates stored in app │
│ ✅ PIN hash stored with bcrypt (same as password) │
│ │
└─────────────────────────────────────────────────────────────┘
Error CodeDescriptionUser Action
LAErrorBiometryNotAvailableNo biometric hardwareUse password
LAErrorBiometryNotEnrolledNo fingerprints registeredSetup in System Preferences
LAErrorBiometryLockoutToo many failed attemptsWait or use password
LAErrorUserCancelUser cancelledRetry or use password
LAErrorUserFallbackUser chose passwordShow password input

// User can enable/disable in settings
interface BiometricSettings {
enabled: boolean;
useForLogin: boolean;
useForSensitiveActions: boolean; // e.g., delete patient
requirePasswordEvery: '24h' | '7d' | 'never';
}