├── src ├── platform │ ├── macos │ │ ├── mod.rs │ │ ├── auth.rs │ │ └── auth_helper.swift │ ├── linux │ │ └── mod.rs │ ├── windows │ │ └── mod.rs │ └── mod.rs ├── auth │ ├── linux.rs │ ├── windows.rs │ └── mod.rs └── main.rs ├── .pinact.yml ├── docs ├── dialog.png └── no-verify.png ├── .gitignore ├── CODEOWNERS ├── .github ├── release.yml └── workflows │ └── ci.yml ├── Cargo.toml ├── LICENSE ├── examples └── pre-commit-hook ├── scripts ├── create-issues.sh └── release.sh ├── Makefile ├── example_usage.sh ├── tests ├── cli_test.rs └── integration_test.rs ├── test_touchid.sh └── README.md /src/platform/macos/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | -------------------------------------------------------------------------------- /.pinact.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - dtolnay/rust-toolchain -------------------------------------------------------------------------------- /docs/dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azu/confirm-pam/HEAD/docs/dialog.png -------------------------------------------------------------------------------- /docs/no-verify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azu/confirm-pam/HEAD/docs/no-verify.png -------------------------------------------------------------------------------- /src/platform/linux/mod.rs: -------------------------------------------------------------------------------- 1 | // Linux platform implementation 2 | // TODO: Implement Linux PAM authentication 3 | -------------------------------------------------------------------------------- /src/platform/windows/mod.rs: -------------------------------------------------------------------------------- 1 | // Windows platform implementation 2 | // TODO: Implement Windows Hello authentication 3 | -------------------------------------------------------------------------------- /src/platform/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(target_os = "macos")] 2 | pub mod macos; 3 | 4 | #[cfg(target_os = "linux")] 5 | pub mod linux; 6 | 7 | #[cfg(target_os = "windows")] 8 | pub mod windows; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | ~/ 3 | # Rust build artifacts 4 | /target/ 5 | Cargo.lock 6 | 7 | # macOS 8 | .DS_Store 9 | 10 | # IDE 11 | .idea/ 12 | .vscode/ 13 | *.swp 14 | *.swo 15 | 16 | # Build outputs 17 | *.o 18 | *.a 19 | 20 | # Test outputs 21 | *.log 22 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Platform-specific code ownership 2 | /src/platform/macos/ @macos-team 3 | /src/platform/linux/ @linux-team 4 | /src/platform/windows/ @windows-team 5 | 6 | # Build configuration 7 | /build.rs @build-team 8 | 9 | # Core authentication logic 10 | /src/auth/ @security-team 11 | 12 | # Documentation 13 | *.md @docs-team -------------------------------------------------------------------------------- /src/auth/linux.rs: -------------------------------------------------------------------------------- 1 | use super::BiometricAuthenticator; 2 | use anyhow::Result; 3 | 4 | pub struct LinuxAuthenticator; 5 | 6 | impl LinuxAuthenticator { 7 | pub fn new() -> Result { 8 | Ok(LinuxAuthenticator) 9 | } 10 | } 11 | 12 | impl BiometricAuthenticator for LinuxAuthenticator { 13 | fn authenticate(&self, _message: &str) -> Result { 14 | // TODO: Implement PAM authentication 15 | Err(anyhow::anyhow!( 16 | "Linux PAM authentication not yet implemented" 17 | )) 18 | } 19 | 20 | fn is_available(&self) -> Result { 21 | // TODO: Check if fprintd is available 22 | Ok(false) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/auth/windows.rs: -------------------------------------------------------------------------------- 1 | use super::BiometricAuthenticator; 2 | use anyhow::Result; 3 | 4 | pub struct WindowsAuthenticator; 5 | 6 | impl WindowsAuthenticator { 7 | pub fn new() -> Result { 8 | Ok(WindowsAuthenticator) 9 | } 10 | } 11 | 12 | impl BiometricAuthenticator for WindowsAuthenticator { 13 | fn authenticate(&self, _message: &str) -> Result { 14 | // TODO: Implement Windows Hello authentication 15 | Err(anyhow::anyhow!( 16 | "Windows Hello authentication not yet implemented" 17 | )) 18 | } 19 | 20 | fn is_available(&self) -> Result { 21 | // TODO: Check if Windows Hello is available 22 | Ok(false) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - 'Type: Meta' 5 | - 'Type: Question' 6 | - 'Type: Release' 7 | 8 | categories: 9 | - title: Security Fixes 10 | labels: ['Type: Security'] 11 | - title: Breaking Changes 12 | labels: ['Type: Breaking Change'] 13 | - title: Features 14 | labels: ['Type: Feature'] 15 | - title: Bug Fixes 16 | labels: ['Type: Bug'] 17 | - title: Documentation 18 | labels: ['Type: Documentation'] 19 | - title: Refactoring 20 | labels: ['Type: Refactoring'] 21 | - title: Testing 22 | labels: ['Type: Testing'] 23 | - title: Maintenance 24 | labels: ['Type: Maintenance'] 25 | - title: CI 26 | labels: ['Type: CI'] 27 | - title: Dependency Updates 28 | labels: ['Type: Dependencies', "dependencies"] 29 | - title: Other Changes 30 | labels: ['*'] 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | test: 14 | name: Test 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 19 | 20 | - name: Install Rust 21 | uses: actions-rust-lang/setup-rust-toolchain@fb51252c7ba57d633bc668f941da052e410add48 # v1.13.0 22 | with: 23 | toolchain: stable 24 | 25 | - name: Run tests 26 | run: cargo test 27 | env: 28 | CI: true # This will skip Touch ID tests in CI 29 | 30 | - name: Check formatting 31 | run: cargo fmt -- --check 32 | 33 | - name: Run clippy 34 | run: cargo clippy -- -D warnings 35 | 36 | - name: Build 37 | run: cargo build --release 38 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "confirm-pam" 3 | version = "0.6.0" 4 | edition = "2024" 5 | authors = ["azu "] 6 | description = "A CLI tool for biometric authentication confirmation" 7 | license = "MIT" 8 | repository = "https://github.com/azu/confirm-pam" 9 | homepage = "https://github.com/azu/confirm-pam" 10 | documentation = "https://github.com/azu/confirm-pam" 11 | readme = "README.md" 12 | keywords = ["cli", "biometric", "authentication", "touchid", "security"] 13 | categories = ["command-line-utilities", "authentication"] 14 | 15 | [dependencies] 16 | clap = { version = "4.5", features = ["derive"] } 17 | anyhow = "1.0" 18 | 19 | [target.'cfg(target_os = "macos")'.dependencies] 20 | 21 | [target.'cfg(target_os = "linux")'.dependencies] 22 | pam = "0.8" 23 | 24 | [target.'cfg(target_os = "windows")'.dependencies] 25 | windows = { version = "0.58", features = ["Win32_Security_Credentials", "Security_Credentials_UI"] } 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 azu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | #[cfg(target_os = "linux")] 4 | mod linux; 5 | #[cfg(target_os = "windows")] 6 | mod windows; 7 | 8 | #[cfg(target_os = "macos")] 9 | pub use crate::platform::macos::auth::MacOSAuthenticator as PlatformAuthenticator; 10 | #[cfg(target_os = "linux")] 11 | pub use linux::LinuxAuthenticator as PlatformAuthenticator; 12 | #[cfg(target_os = "windows")] 13 | pub use windows::WindowsAuthenticator as PlatformAuthenticator; 14 | 15 | pub trait BiometricAuthenticator { 16 | fn authenticate(&self, message: &str) -> Result; 17 | fn is_available(&self) -> Result; 18 | } 19 | 20 | #[cfg(test)] 21 | mod tests { 22 | use super::*; 23 | 24 | #[test] 25 | fn test_platform_authenticator_creation() { 26 | let result = PlatformAuthenticator::new(); 27 | assert!(result.is_ok()); 28 | } 29 | 30 | #[test] 31 | #[cfg(target_os = "macos")] 32 | fn test_macos_authenticator_is_available() { 33 | let authenticator = PlatformAuthenticator::new().unwrap(); 34 | let result = authenticator.is_available(); 35 | assert!(result.is_ok()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/platform/macos/auth.rs: -------------------------------------------------------------------------------- 1 | use crate::auth::BiometricAuthenticator; 2 | use anyhow::Result; 3 | use std::ffi::CString; 4 | 5 | unsafe extern "C" { 6 | fn authenticate_with_biometrics(message: *const std::os::raw::c_char) -> i32; 7 | fn is_biometrics_available() -> bool; 8 | } 9 | 10 | pub struct MacOSAuthenticator; 11 | 12 | impl MacOSAuthenticator { 13 | pub fn new() -> Result { 14 | Ok(MacOSAuthenticator) 15 | } 16 | } 17 | 18 | impl BiometricAuthenticator for MacOSAuthenticator { 19 | fn authenticate(&self, message: &str) -> Result { 20 | let c_message = CString::new(message)?; 21 | unsafe { 22 | match authenticate_with_biometrics(c_message.as_ptr()) { 23 | 0 => Ok(true), // Success 24 | 1 => Ok(false), // User cancelled 25 | 2 => Err(anyhow::anyhow!("Authentication failed")), 26 | 3 => Err(anyhow::anyhow!("Biometric authentication not available")), 27 | _ => Err(anyhow::anyhow!("Unknown authentication error")), 28 | } 29 | } 30 | } 31 | 32 | fn is_available(&self) -> Result { 33 | unsafe { Ok(is_biometrics_available()) } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/pre-commit-hook: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Example pre-commit hook that requires Touch ID authentication for --no-verify 3 | # 4 | # Installation: 5 | # 1. Copy this file to .git/hooks/pre-commit 6 | # 2. Make it executable: chmod +x .git/hooks/pre-commit 7 | # 3. Make sure confirm-pam is in your PATH 8 | 9 | # Check if running with --no-verify 10 | if [ -n "$SKIP_VERIFY" ] || git rev-parse --verify HEAD >/dev/null 2>&1 && [ "$1" = "--no-verify" ]; then 11 | echo "⚠️ Attempting to bypass pre-commit hooks with --no-verify" 12 | 13 | # Require Touch ID authentication 14 | if ! confirm-pam "Allow bypassing git hooks with --no-verify?"; then 15 | echo "❌ Touch ID authentication required to use --no-verify" 16 | echo "💡 Tip: Remove --no-verify to run normal commit with hooks" 17 | exit 1 18 | fi 19 | 20 | echo "✅ Touch ID authenticated - bypassing hooks" 21 | exit 0 22 | fi 23 | 24 | # Normal pre-commit checks go here 25 | echo "Running pre-commit hooks..." 26 | 27 | # Example: Check for trailing whitespace 28 | if git diff --cached --check; then 29 | echo "✅ No trailing whitespace found" 30 | else 31 | echo "❌ Trailing whitespace detected. Please fix before committing." 32 | exit 1 33 | fi 34 | 35 | # Add more pre-commit checks as needed -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use std::process; 4 | 5 | mod auth; 6 | mod platform; 7 | use auth::{BiometricAuthenticator, PlatformAuthenticator}; 8 | 9 | #[derive(Parser, Debug)] 10 | #[command( 11 | name = "confirm-pam", 12 | about = "A CLI tool for biometric authentication confirmation", 13 | version 14 | )] 15 | struct Args { 16 | /// Message to display during authentication 17 | message: String, 18 | } 19 | 20 | fn main() { 21 | let args = Args::parse(); 22 | 23 | match run(&args.message) { 24 | Ok(authenticated) => { 25 | if authenticated { 26 | process::exit(0); 27 | } else { 28 | eprintln!("Authentication failed"); 29 | process::exit(1); 30 | } 31 | } 32 | Err(e) => { 33 | eprintln!("Error: {e}"); 34 | process::exit(2); 35 | } 36 | } 37 | } 38 | 39 | fn run(message: &str) -> Result { 40 | let authenticator = PlatformAuthenticator::new()?; 41 | 42 | if !authenticator.is_available()? { 43 | return Err(anyhow::anyhow!( 44 | "Biometric authentication is not available on this device" 45 | )); 46 | } 47 | 48 | authenticator.authenticate(message) 49 | } 50 | -------------------------------------------------------------------------------- /scripts/create-issues.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Script to create GitHub issues for Linux and Windows support 4 | 5 | echo "Creating GitHub issues for platform support..." 6 | 7 | # Check if gh CLI is installed 8 | if ! command -v gh &> /dev/null; then 9 | echo "❌ GitHub CLI (gh) is not installed. Please install it first:" 10 | echo " brew install gh" 11 | echo " or visit: https://cli.github.com/" 12 | exit 1 13 | fi 14 | 15 | # Check if we're in a git repository 16 | if ! git rev-parse --git-dir > /dev/null 2>&1; then 17 | echo "❌ This is not a git repository" 18 | exit 1 19 | fi 20 | 21 | # Create Linux support issue 22 | echo "Creating Linux PAM integration issue..." 23 | gh issue create \ 24 | --title "Add Linux PAM integration for biometric authentication" \ 25 | --body-file .github/ISSUE_TEMPLATE/linux_support.md \ 26 | --label "enhancement,linux,help wanted" 27 | 28 | # Create Windows support issue 29 | echo "Creating Windows Hello integration issue..." 30 | gh issue create \ 31 | --title "Add Windows Hello integration for biometric authentication" \ 32 | --body-file .github/ISSUE_TEMPLATE/windows_support.md \ 33 | --label "enhancement,windows,help wanted" 34 | 35 | echo "✅ Issues created successfully!" 36 | echo "You can view them at: https://github.com/azu/confirm-pam/issues" -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help build test clean install release-patch release-minor release-major 2 | 3 | # Default target 4 | help: 5 | @echo "Available targets:" 6 | @echo " build - Build the project" 7 | @echo " test - Run tests" 8 | @echo " release-patch - Create a patch release (0.1.0 → 0.1.1)" 9 | @echo " release-minor - Create a minor release (0.1.0 → 0.2.0)" 10 | @echo " release-major - Create a major release (0.1.0 → 1.0.0)" 11 | @echo " clean - Clean build artifacts" 12 | @echo " install - Install binary to /usr/local/bin" 13 | @echo " help - Show this help message" 14 | 15 | # Build the project 16 | build: 17 | cargo build --release 18 | 19 | # Run tests 20 | test: 21 | cargo test 22 | cargo clippy -- -D warnings 23 | cargo fmt -- --check 24 | 25 | # Clean build artifacts 26 | clean: 27 | cargo clean 28 | 29 | # Install binary to system PATH 30 | install: build 31 | sudo cp target/release/confirm-pam /usr/local/bin/ 32 | 33 | # Create a patch release 34 | release-patch: test 35 | @./scripts/release.sh patch 36 | 37 | # Create a minor release 38 | release-minor: test 39 | @./scripts/release.sh minor 40 | 41 | # Create a major release 42 | release-major: test 43 | @./scripts/release.sh major 44 | 45 | 46 | # Quick development commands 47 | dev-test: 48 | ./test_touchid.sh 49 | 50 | check: test -------------------------------------------------------------------------------- /example_usage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Example usage of confirm-pam 4 | 5 | # Build the project 6 | cargo build --release 7 | 8 | # Example 1: Git commit hook simulation 9 | echo "Example 1: Git commit hook with --no-verify protection" 10 | echo "Please authenticate with Touch ID when prompted..." 11 | if ./target/release/confirm-pam "Allow git commit --no-verify?"; then 12 | echo "✅ Touch ID authenticated - proceeding with git commit --no-verify" 13 | else 14 | echo "❌ Touch ID authentication failed or cancelled - blocking git commit --no-verify" 15 | fi 16 | 17 | # Example 2: Sensitive operation confirmation 18 | echo -e "\nExample 2: Sensitive operation confirmation" 19 | echo "Please authenticate with Touch ID when prompted (or cancel to test rejection)..." 20 | if ./target/release/confirm-pam "Delete production database?"; then 21 | echo "⚠️ User confirmed dangerous operation with Touch ID" 22 | else 23 | echo "✅ Operation cancelled by user or Touch ID failed" 24 | fi 25 | 26 | # Example 3: Check if Touch ID is available 27 | echo -e "\nExample 3: Checking Touch ID availability" 28 | ./target/release/confirm-pam "Test availability" 2>/dev/null 29 | case $? in 30 | 0) echo "✅ Touch ID is available and authentication succeeded" ;; 31 | 1) echo "❌ Touch ID is available but user cancelled" ;; 32 | 2) echo "❌ Touch ID authentication error occurred" ;; 33 | *) echo "❌ Unknown error" ;; 34 | esac -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | RELEASE_TYPE=$1 5 | 6 | if [ -z "$RELEASE_TYPE" ]; then 7 | echo "Usage: $0 " 8 | exit 1 9 | fi 10 | 11 | # Check if we have semver command 12 | if ! command -v semver &> /dev/null; then 13 | echo "Installing semver..." 14 | npm install -g semver 15 | fi 16 | 17 | # Get current version from Cargo.toml 18 | CURRENT_VERSION=$(grep '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/') 19 | echo "Current version: $CURRENT_VERSION" 20 | 21 | # Calculate new version 22 | NEW_VERSION=$(semver -i $RELEASE_TYPE $CURRENT_VERSION) 23 | echo "New version: $NEW_VERSION" 24 | 25 | # Update Cargo.toml 26 | sed -i.bak "s/^version = \".*\"/version = \"$NEW_VERSION\"/" Cargo.toml 27 | rm Cargo.toml.bak 28 | 29 | echo "Updated Cargo.toml to version $NEW_VERSION" 30 | 31 | # Check if there are any changes 32 | if ! git diff --quiet; then 33 | echo "Committing version bump..." 34 | git add Cargo.toml 35 | git commit -m "chore: release v$NEW_VERSION" 36 | else 37 | echo "No changes to commit" 38 | fi 39 | 40 | # Create and push tag 41 | echo "Creating tag v$NEW_VERSION..." 42 | git tag -a "v$NEW_VERSION" -m "Release v$NEW_VERSION" 43 | 44 | echo "Pushing to origin..." 45 | git push origin main 46 | git push origin "v$NEW_VERSION" 47 | 48 | # Publish to crates.io 49 | echo "Publishing to crates.io..." 50 | cargo publish 51 | 52 | echo "✅ Release v$NEW_VERSION completed successfully!" 53 | echo "🚀 Published to crates.io" 54 | echo "📦 Tagged and pushed to GitHub" 55 | echo "" 56 | echo "Next steps:" 57 | echo "- Create GitHub release manually if needed" 58 | echo "- Update any documentation or announcements" -------------------------------------------------------------------------------- /tests/cli_test.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process::Command; 3 | 4 | #[test] 5 | fn test_missing_argument() { 6 | let exe_path = env::current_exe() 7 | .unwrap() 8 | .parent() 9 | .unwrap() 10 | .parent() 11 | .unwrap() 12 | .join("confirm-pam"); 13 | 14 | let output = Command::new(&exe_path) 15 | .output() 16 | .expect("Failed to execute command"); 17 | 18 | assert!(!output.status.success()); 19 | assert_eq!(output.status.code(), Some(2)); 20 | 21 | let stderr = String::from_utf8_lossy(&output.stderr); 22 | assert!(stderr.contains("error: the following required arguments were not provided")); 23 | } 24 | 25 | #[test] 26 | fn test_help_message() { 27 | let exe_path = env::current_exe() 28 | .unwrap() 29 | .parent() 30 | .unwrap() 31 | .parent() 32 | .unwrap() 33 | .join("confirm-pam"); 34 | 35 | let output = Command::new(&exe_path) 36 | .args(&["--help"]) 37 | .output() 38 | .expect("Failed to execute command"); 39 | 40 | let stdout = String::from_utf8_lossy(&output.stdout); 41 | assert!(stdout.contains("A CLI tool for biometric authentication confirmation")); 42 | assert!(stdout.contains("Message to display during authentication")); 43 | } 44 | 45 | #[test] 46 | fn test_version() { 47 | let exe_path = env::current_exe() 48 | .unwrap() 49 | .parent() 50 | .unwrap() 51 | .parent() 52 | .unwrap() 53 | .join("confirm-pam"); 54 | 55 | let output = Command::new(&exe_path) 56 | .args(&["--version"]) 57 | .output() 58 | .expect("Failed to execute command"); 59 | 60 | let stdout = String::from_utf8_lossy(&output.stdout); 61 | assert!(stdout.contains("confirm-pam")); 62 | } 63 | -------------------------------------------------------------------------------- /src/platform/macos/auth_helper.swift: -------------------------------------------------------------------------------- 1 | import LocalAuthentication 2 | import Foundation 3 | 4 | // Return codes: 0 = success, 1 = user cancelled, 2 = failed, 3 = not available 5 | @_cdecl("authenticate_with_biometrics") 6 | public func authenticateWithBiometrics(_ messagePtr: UnsafePointer) -> Int32 { 7 | let message = String(cString: messagePtr) 8 | let context = LAContext() 9 | var error: NSError? 10 | 11 | // Check if biometric authentication is available 12 | guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else { 13 | print("Biometrics not available: \(error?.localizedDescription ?? "Unknown error")") 14 | return 3 // Not available 15 | } 16 | 17 | var authResult: Int32 = 2 // Default to failed 18 | let semaphore = DispatchSemaphore(value: 0) 19 | 20 | // Perform authentication 21 | context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, 22 | localizedReason: message) { success, authError in 23 | if success { 24 | authResult = 0 // Success 25 | } else if let error = authError as NSError? { 26 | if error.code == LAError.userCancel.rawValue { 27 | authResult = 1 // User cancelled 28 | } else { 29 | print("Authentication error: \(error.localizedDescription)") 30 | authResult = 2 // Failed 31 | } 32 | } 33 | semaphore.signal() 34 | } 35 | 36 | // Wait for authentication to complete 37 | semaphore.wait() 38 | return authResult 39 | } 40 | 41 | @_cdecl("is_biometrics_available") 42 | public func isBiometricsAvailable() -> Bool { 43 | let context = LAContext() 44 | var error: NSError? 45 | return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) 46 | } -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::process::Command; 3 | 4 | #[test] 5 | #[cfg(target_os = "macos")] 6 | fn test_touch_id_availability() { 7 | // Skip in CI environment 8 | if env::var("CI").is_ok() { 9 | println!("Skipping Touch ID test in CI environment"); 10 | return; 11 | } 12 | 13 | let output = Command::new("cargo") 14 | .args(&["run", "--", "Test Touch ID availability"]) 15 | .output() 16 | .expect("Failed to execute command"); 17 | 18 | // Should either succeed (0), be cancelled by user (1), or error (2) if Touch ID unavailable 19 | let exit_code = output.status.code().unwrap_or(99); 20 | assert!( 21 | exit_code == 0 || exit_code == 1 || exit_code == 2, 22 | "Expected success (0), cancel (1), or error (2), got {}", 23 | exit_code 24 | ); 25 | } 26 | 27 | #[test] 28 | fn test_empty_message() { 29 | // Skip empty message test as clap requires at least one argument 30 | // This is expected behavior - the CLI requires a message argument 31 | } 32 | 33 | #[test] 34 | fn test_long_message() { 35 | let long_message = "This is a very long message ".repeat(10); 36 | let output = Command::new("cargo") 37 | .args(&["run", "--", &long_message]) 38 | .output() 39 | .expect("Failed to execute command"); 40 | 41 | // Long message should be handled gracefully 42 | assert!(output.status.code().is_some()); 43 | } 44 | 45 | #[test] 46 | fn test_special_characters_in_message() { 47 | let special_message = "Test with special chars: 日本語 🔐 \"quotes\" 'apostrophe' \n newline"; 48 | let output = Command::new("cargo") 49 | .args(&["run", "--", special_message]) 50 | .output() 51 | .expect("Failed to execute command"); 52 | 53 | // Special characters should be handled properly 54 | assert!(output.status.code().is_some()); 55 | } 56 | -------------------------------------------------------------------------------- /test_touchid.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Test script for Touch ID functionality 4 | echo "=== confirm-pam Touch ID Test Suite ===" 5 | echo 6 | 7 | # Colors for output 8 | GREEN='\033[0;32m' 9 | RED='\033[0;31m' 10 | YELLOW='\033[1;33m' 11 | NC='\033[0m' # No Color 12 | 13 | # Test 1: Authentication Success 14 | echo "Test 1: Authentication Success" 15 | echo "Please authenticate with Touch ID when prompted..." 16 | ./target/release/confirm-pam "Test 1: Please authenticate with Touch ID" 17 | EXIT_CODE=$? 18 | if [ $EXIT_CODE -eq 0 ]; then 19 | echo -e "${GREEN}✅ Test 1 PASSED: Authentication successful${NC}" 20 | elif [ $EXIT_CODE -eq 1 ]; then 21 | echo -e "${YELLOW}⚠️ Test 1: User cancelled (this is also a valid outcome)${NC}" 22 | else 23 | echo -e "${RED}❌ Test 1 FAILED: Unexpected exit code $EXIT_CODE${NC}" 24 | fi 25 | echo 26 | 27 | # Test 2: User Cancellation 28 | echo "Test 2: User Cancellation" 29 | echo "Please CANCEL the Touch ID prompt when it appears..." 30 | ./target/release/confirm-pam "Test 2: Please CANCEL this Touch ID prompt" 31 | EXIT_CODE=$? 32 | if [ $EXIT_CODE -eq 1 ]; then 33 | echo -e "${GREEN}✅ Test 2 PASSED: Cancellation detected correctly${NC}" 34 | elif [ $EXIT_CODE -eq 0 ]; then 35 | echo -e "${YELLOW}⚠️ Test 2: User authenticated instead of cancelling${NC}" 36 | else 37 | echo -e "${RED}❌ Test 2 FAILED: Unexpected exit code $EXIT_CODE${NC}" 38 | fi 39 | echo 40 | 41 | # Test 3: Multiple Sequential Authentications 42 | echo "Test 3: Multiple Sequential Authentications" 43 | echo "You will be prompted 3 times. Please authenticate all of them..." 44 | SUCCESS_COUNT=0 45 | for i in 1 2 3; do 46 | ./target/release/confirm-pam "Test 3: Authentication $i of 3" 47 | if [ $? -eq 0 ]; then 48 | ((SUCCESS_COUNT++)) 49 | fi 50 | done 51 | echo -e "${GREEN}✅ Successfully authenticated $SUCCESS_COUNT out of 3 times${NC}" 52 | echo 53 | 54 | # Test 4: Special Characters 55 | echo "Test 4: Special Characters in Message" 56 | ./target/release/confirm-pam "Test 4: 日本語 🔐 \"quotes\" 'apostrophe'" 57 | EXIT_CODE=$? 58 | if [ $EXIT_CODE -eq 0 ] || [ $EXIT_CODE -eq 1 ]; then 59 | echo -e "${GREEN}✅ Test 4 PASSED: Special characters handled correctly${NC}" 60 | else 61 | echo -e "${RED}❌ Test 4 FAILED: Special characters caused an error${NC}" 62 | fi 63 | echo 64 | 65 | echo "=== Test Suite Complete ==="" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # confirm-pam 2 | 3 | A CLI tool for biometric authentication confirmation. 4 | 5 | ## Overview 6 | 7 | `confirm-pam` is a command-line tool that provides biometric authentication (fingerprint/Touch ID) confirmation prompts. It's designed to prevent automated tools (including AI agents) from bypassing security checks like `git commit --no-verify`. 8 | 9 | ``` 10 | $ confirm-pam "Allow commit with --no-verify?"; 11 | ``` 12 | 13 | shows following dialog: 14 | 15 | ![`confirm-pam "Allow commit with --no-verify?";`](./docs/dialog.png) 16 | 17 | ## Features 18 | 19 | - ✅ Touch ID authentication on macOS 20 | - 🔐 Custom message display during authentication prompt 21 | - 🚦 Simple exit codes: 0 for success, 1 for failure/cancel, 2 for errors 22 | - 🪶 Lightweight binary with minimal dependencies 23 | - 🚀 Fast native performance 24 | 25 | ## Installation 26 | 27 | ### From crates.io (Recommended) 28 | 29 | ```bash 30 | cargo install confirm-pam 31 | ``` 32 | 33 | ### From Source 34 | 35 | ```bash 36 | # Clone and build 37 | git clone https://github.com/azu/confirm-pam 38 | cd confirm-pam 39 | cargo build --release 40 | 41 | # Copy to PATH 42 | sudo cp target/release/confirm-pam /usr/local/bin/ 43 | ``` 44 | 45 | ## Usage 46 | 47 | ### Basic Usage 48 | 49 | ```bash 50 | confirm-pam "Your authentication message here" 51 | ``` 52 | 53 | ### Exit Codes 54 | 55 | - `0` - Authentication successful 56 | - `1` - User cancelled authentication 57 | - `2` - Error occurred (biometrics unavailable, etc.) 58 | 59 | ### Shell Script Integration 60 | 61 | ```bash 62 | if confirm-pam "Allow sensitive operation?"; then 63 | echo "Authenticated - proceeding" 64 | else 65 | echo "Authentication failed or cancelled" 66 | exit 1 67 | fi 68 | ``` 69 | 70 | ### Git Hook Example 71 | 72 | Override `git` command to include confirmation prompts. 73 | 74 | ![git commit --no-verify example](./docs/no-verify.png) 75 | 76 | zsh example: 77 | 78 | ```zsh 79 | # Override git push -f and commit --no-verify 80 | git() { 81 | if [[ $@ == *'push'* && $@ == *'-f'* ]]; then 82 | echo "Use git push --force-with-lease --force-if-includes instead" 83 | elif [[ $@ == *'commit'* && $@ == *'--no-verify'* ]]; then 84 | if confirm-pam "Allow commit with --no-verify?"; then 85 | command git "$@" 86 | else 87 | echo "Authenticated Failed - Disallow the commit by human. Please wait and follow the instructions." 88 | return 1 89 | fi 90 | else 91 | command git "$@" 92 | fi 93 | } 94 | ``` 95 | 96 | ## Platform Support 97 | 98 | | Platform | Status | Authentication Method | Issue | 99 | | -------- | ------------ | --------------------- | ------------------------------------------------- | 100 | | macOS | ✅ Supported | Touch ID | - | 101 | | Linux | 🚧 Planned | PAM + fprintd | [#1](https://github.com/azu/confirm-pam/issues/1) | 102 | | Windows | 🚧 Planned | Windows Hello | [#2](https://github.com/azu/confirm-pam/issues/2) | 103 | 104 | ## Requirements 105 | 106 | ### macOS 107 | 108 | - macOS 10.12.2 or later 109 | - Touch ID capable device 110 | - Touch ID must be configured in System Preferences 111 | 112 | ## Development 113 | 114 | ### Using Make Commands 115 | 116 | ```bash 117 | # Show available commands 118 | make help 119 | 120 | # Build the project 121 | make build 122 | 123 | # Run all tests (unit + lint + format check) 124 | make test 125 | 126 | # Run Touch ID integration tests (requires user interaction) 127 | make dev-test 128 | 129 | # Clean build artifacts 130 | make clean 131 | 132 | # Install to system PATH 133 | make install 134 | 135 | # Create releases with specific version bump 136 | make release-patch # patch version 137 | make release-minor # minor version 138 | make release-major # major version 139 | ``` 140 | 141 | ### Using Cargo Directly 142 | 143 | ```bash 144 | # Run tests 145 | cargo test 146 | 147 | # Run integration tests (requires user interaction) 148 | ./test_touchid.sh 149 | 150 | # Build for release 151 | cargo build --release 152 | 153 | # Format code 154 | cargo fmt 155 | 156 | # Run linter 157 | cargo clippy 158 | ``` 159 | 160 | ## Release Process 161 | 162 | This project uses **local manual releases** with make commands: 163 | 164 | ### Release Commands 165 | 166 | ```bash 167 | # Patch release (0.1.0 → 0.1.1): Bug fixes, small improvements 168 | make release-patch 169 | 170 | # Minor release (0.1.0 → 0.2.0): New features, enhancements 171 | make release-minor 172 | 173 | # Major release (0.1.0 → 1.0.0): Breaking changes, major releases 174 | make release-major 175 | ``` 176 | 177 | ### What happens automatically 178 | 179 | 1. ✅ Version bumped in `Cargo.toml` using semver 180 | 2. ✅ Git tag created and pushed 181 | 3. ✅ Published to crates.io 182 | 4. ⚠️ GitHub release needs to be created manually 183 | 184 | ## Contributing 185 | 186 | Contributions are welcome! Please see the [open issues](https://github.com/azu/confirm-pam/issues) for planned features and improvements. 187 | 188 | ## License 189 | 190 | MIT License - See [LICENSE](LICENSE) file for details 191 | --------------------------------------------------------------------------------