Skip to content

Repository files navigation

Swift GnuPG

A comprehensive Swift wrapper for GnuPG operations, providing a modern, async/await interface for cryptographic operations including signing, verification, encryption, decryption, and key management.

This is a Swift port of python-gnupg, maintaining API compatibility while embracing Swift's concurrency model and type safety.

Swift 6.0 Platform License

⚠️ Important Prerequisite: This library requires GnuPG 2.0+ to be installed on your system. Swift GnuPG acts as a wrapper around the gpg command-line tool and cannot function without it.

Features

Complete GPG Operations Suite

  • Digital signature creation and verification
  • Public key and symmetric encryption/decryption
  • Comprehensive key management (list, import, export, generate, delete)
  • Support for all GPG options (ASCII armor, detached signatures, etc.)

Modern Swift Design

  • Full async/await support with proper concurrency
  • Strong typing with comprehensive error handling
  • Sendable compliance for safe concurrent usage
  • Swift Testing framework integration

Robust Implementation

  • Automatic GPG binary discovery across platforms
  • Comprehensive logging with multiple backends (Console, OSLog)
  • Detailed status message parsing
  • Memory-safe process management

Production Ready

  • Extensive test coverage with real GPG integration
  • Comprehensive error types with recovery suggestions
  • Performance optimized with configurable options
  • Python-gnupg API compatibility

Installation

Swift Package Manager

Add the following to your Package.swift file:

dependencies: [
    .package(url: "https://github.com/Kingpin-Apps/swift-gnupg.git", from: "0.1.0")
]

Requirements

  • Swift 6.0+
  • GnuPG 2.0+ installed on the system
  • macOS 10.15+ or Linux

Installing GnuPG

macOS:

brew install gnupg

Ubuntu/Debian:

sudo apt install gnupg

RHEL/CentOS:

sudo yum install gnupg2

Quick Start

Basic Usage

import GnuPG

// Initialize GPG (automatically finds GPG binary)
let gpg = try GnuPG()

// List available keys
let keys = await gpg.listKeys()
print("Found \(keys.keys.count) public keys")

// Sign a message
let signResult = await gpg.sign(message: "Hello, World!")
if signResult.isSuccessful {
    print("Message signed successfully!")
}

// Encrypt for a recipient
let encryptResult = await gpg.encrypt(
    message: "Secret message", 
    recipients: ["user@example.com"]
)

// Verify a signature
let verifyResult = await gpg.verify(message: signedMessage)
print("Signature valid: \(verifyResult.valid)")

Advanced Configuration

// Custom GPG configuration
let customGPG = try GnuPG(
    gpgBinary: "/usr/local/bin/gpg",
    gnupgHome: "/path/to/gpg/home",
    verbose: true,
    options: ["--cipher-algo", "AES256", "--digest-algo", "SHA512"]
)

// Enable detailed logging
customGPG.logger.level = .debug
customGPG.logger.setConsoleLogging(enabled: true)

// macOS: Enable OSLog integration
if #available(macOS 11.0, *) {
    customGPG.logger.setOSLogging(enabled: true, subsystem: "com.myapp.crypto")
}

Core Operations

Digital Signatures

// Sign with default key
let signResult = await gpg.sign(message: "Document to sign")

// Sign with specific key and options
let advancedSign = await gpg.sign(
    message: "Important document",
    keyId: "user@example.com",
    passphrase: "secret",
    clearsign: true,
    detach: false,
    armor: true
)

// Sign a file
let fileSignResult = await gpg.signFile(
    filePath: "/path/to/document.txt",
    outputPath: "/path/to/document.txt.sig",
    detach: true
)

Signature Verification

// Verify inline signature
let verifyResult = await gpg.verify(message: signedMessage)
if verifyResult.valid {
    print("Signature by: \(verifyResult.username ?? "unknown")")
    print("Key ID: \(verifyResult.keyId ?? "unknown")")
    print("Fingerprint: \(verifyResult.fingerprint ?? "unknown")")
}

// Verify detached signature
let detachedVerify = await gpg.verifyFile(
    filePath: "/path/to/document.txt",
    signaturePath: "/path/to/document.txt.sig"
)

// Check signature details
for (sigId, info) in verifyResult.sigInfo {
    print("Signature \(sigId): \(info)")
}

Encryption & Decryption

// Public key encryption
let encryptResult = await gpg.encrypt(
    message: "Top secret message",
    recipients: ["alice@example.com", "bob@example.com"],
    armor: true,
    sign: true // Also sign the message
)

// Symmetric encryption
let symmetricResult = await gpg.encryptSymmetric(
    message: "Secret data",
    passphrase: "strong-password"
)

// Decrypt message
let decryptResult = await gpg.decrypt(
    message: encryptedMessage,
    passphrase: "secret"
)

if decryptResult.isSuccessful {
    let plaintext = String(data: decryptResult.data!, encoding: .utf8)
    print("Decrypted: \(plaintext ?? "Unable to decode")")
    
    // Check if decryption also verified a signature
    if decryptResult.signatureValid {
        print("Signature verified by: \(decryptResult.signatureUser ?? "unknown")")
    }
}

Key Management

// List all keys
let publicKeys = await gpg.listKeys()
let secretKeys = await gpg.listKeys(secretKeys: true)

// Find specific keys
if let key = publicKeys.findKey(byId: "alice@example.com") {
    print("Key: \(key.keyId)")
    print("User: \(key.userId ?? "No user ID")")
    print("Capabilities: \(key.capabilities ?? "None")")
    print("Expires: \(key.isExpired ? "Yes" : "No")")
}

// Import keys
let keyData = """
-----BEGIN PGP PUBLIC KEY BLOCK-----
...
-----END PGP PUBLIC KEY BLOCK-----
"""

let importResult = await gpg.importKeys(keyString: keyData)
print("Imported \(importResult.imported) keys")

// Export keys
if let exportedData = await gpg.exportKeys(keyId: "alice@example.com") {
    let exportedString = String(data: exportedData, encoding: .utf8)!
    print("Exported key:\n\(exportedString)")
}

// Generate new key pair
let generateResult = await gpg.generateKey(
    keyType: "RSA",
    keySize: 3072,
    userId: "New User <newuser@example.com>",
    passphrase: "strong-passphrase"
)

// Delete keys
let deleted = await gpg.deleteKey(keyId: "old-key@example.com")

Error Handling

The library provides comprehensive error handling with detailed context and recovery suggestions:

do {
    let gpg = try GnuPG()
    let result = await gpg.sign(message: "test")
    
    if !result.isSuccessful {
        print("Signing failed: \(result.status ?? "unknown error")")
    }
} catch let error as GPGError {
    print("Error: \(error.errorDescription ?? "Unknown error")")
    print("Reason: \(error.failureReason ?? "No specific reason")")
    print("Suggestion: \(error.recoverySuggestion ?? "No suggestion available")")
    
    switch error {
    case .gpgNotAvailable(let binary):
        print("Install GPG or check PATH. Missing binary: \(binary)")
    case .invalidPassphrase(let reason):
        print("Passphrase issue: \(reason)")
    case .keyNotFound(let keyId):
        print("Key not found: \(keyId)")
    case .processTerminated(let code, let stderr):
        print("GPG process failed with code \(code): \(stderr)")
    default:
        print("Other error: \(error)")
    }
}

Logging

Comprehensive logging is available with multiple backends:

import GnuPG

// Configure logging level
GPGLogger.shared.level = .debug  // .debug, .info, .warning, .error, .critical

// Enable console logging
GPGLogger.shared.setConsoleLogging(enabled: true)

// Enable OSLog (macOS 11.0+)
if #available(macOS 11.0, *) {
    GPGLogger.shared.setOSLogging(enabled: true, subsystem: "com.myapp.crypto")
}

// Use operation logging for timing
let result = await GPGLogger.shared.logOperationAsync("Key Generation") {
    return await gpg.generateKey(
        keyType: "RSA",
        keySize: 2048, 
        userId: "test@example.com",
        passphrase: "test123"
    )
}

Custom Log Handlers

struct FileLogHandler: GPGLogHandler {
    private let fileHandle: FileHandle
    
    init(logPath: String) throws {
        self.fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: logPath))
    }
    
    func log(level: GPGLogLevel, message: String, category: String, file: String, function: String, line: Int) {
        let logEntry = "[\(level.description)] \(category): \(message)\n"
        fileHandle.write(logEntry.data(using: .utf8)!)
    }
}

// Add custom handler
let fileHandler = try FileLogHandler(logPath: "/tmp/gpg.log")
GPGLogger.shared.addHandler(fileHandler)

Testing

The library includes comprehensive tests using Swift Testing:

// Run all tests
swift test

// Run specific test suites
swift test --filter "Integration"
swift test --filter "KeyOperations"
swift test --filter "Basic"

// Test with verbose output
swift test --filter "testGPGBinaryDiscoveryAndVersion"

Test Organization

  • BasicGnuPGTests: Core functionality without GPG binary
  • KeyOperationsTests: Key management operations
  • SignOperationsTests: Digital signature operations
  • VerifyOperationsTests: Signature verification
  • EncryptDecryptTests: Encryption and decryption
  • IntegrationTests: End-to-end workflows with real GPG

Architecture

Sources/GnuPG/
├── GnuPG.swift                 # Main class with process management
├── Core/
│   ├── GPGTypes.swift         # Version, errors, and type definitions
│   ├── GPGLogger.swift        # Comprehensive logging system
│   └── StatusHandler.swift    # GPG status message processing
├── Results/                   # Result classes for each operation
│   ├── SignResult.swift
│   ├── VerifyResult.swift
│   ├── EncryptResult.swift
│   ├── DecryptResult.swift
│   ├── ListKeysResult.swift   # Includes GPGKey struct
│   └── ImportResult.swift
├── Operations/               # Operation implementations
│   ├── SignOperations.swift
│   ├── VerifyOperations.swift
│   ├── EncryptOperations.swift
│   ├── DecryptOperations.swift
│   └── KeyOperations.swift
└── Utilities/
    └── GPGUtilities.swift    # Helper functions and validation

Performance Considerations

  • Process Reuse: Each operation creates a new GPG process for security and reliability
  • Memory Management: Proper cleanup of file handles and process resources
  • Streaming: Large file operations use streaming to minimize memory usage
  • Concurrency: All operations are async and can be run concurrently
  • Buffering: Configurable buffer sizes for optimal performance
// Configure for large files
gpg.bufferSize = 65536  // 64KB buffer for large operations

// Concurrent operations
async let signTask = gpg.sign(message: "message1")
async let encryptTask = gpg.encrypt(message: "message2", recipients: ["user@example.com"])

let signResult = await signTask
let encryptResult = await encryptTask

Troubleshooting

Common Issues

GPG Not Found:

Error: GPG binary not available: gpg. Please install GPG or specify the correct path.

Solution: Install GPG or specify the full path:

let gpg = try GnuPG(gpgBinary: "/usr/local/bin/gpg")

Permission Denied:

Error: Permission denied for operation: signing

Solution: Check GPG home directory permissions and key access.

Key Not Found:

Error: Key not found: user@example.com

Solution: Import the key first or check the key identifier.

Passphrase Issues:

Error: Invalid passphrase: contains newline or null characters

Solution: Ensure passphrase doesn't contain special characters.

Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes with tests
  4. Run the test suite (swift test)
  5. Commit your changes (git commit -m 'Add amazing feature')
  6. Push to the branch (git push origin feature/amazing-feature)
  7. Open a Pull Request

Development Setup

git clone https://github.com/your-org/swift-gnupg.git
cd swift-gnupg

# Install dependencies
brew install gnupg

# Run tests
swift test

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Based on python-gnupg by Vinay Sajip
  • Inspired by the need for a modern Swift cryptography interface
  • Built with Swift's modern concurrency features

Changelog

1.0.0 (2024-XX-XX)

  • Initial release with complete GPG operations
  • Full async/await support
  • Comprehensive error handling and logging
  • Production-ready with extensive test coverage

For more detailed examples and API documentation, see the Documentation directory.

About

Swift wrapper for GnuPG

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages