Swift Platform Guide
This guide covers the installation, setup, and usage of MetaMUI Crypto Primitives in Swift projects for iOS, macOS, watchOS, and tvOS.
Installation
Build from source:
cd metamui-crypto-swift
swift build
Running Tests
cd metamui-crypto-swift
swift run TestFalconSwift
Using in Your Project
To use as a local dependency, add a path reference in your Package.swift:
// swift-tools-version:5.7
import PackageDescription
let package = Package(
name: "YourProject",
platforms: [
.iOS(.v13),
.macOS(.v10_15),
.watchOS(.v6),
.tvOS(.v13)
],
dependencies: [
.package(path: "../metamui-crypto-swift")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "MetaMUICrypto", package: "metamui-crypto-swift")
]
)
]
)
Quick Start
import MetaMUICrypto
// Generate Ed25519 keypair
let keypair = try Ed25519.generateKeypair()
// Sign a message
let message = "Hello, MetaMUI!".data(using: .utf8)!
let signature = try Ed25519.sign(message, privateKey: keypair.privateKey)
// Verify signature
let isValid = try Ed25519.verify(signature, message: message, publicKey: keypair.publicKey)
print("Signature valid: \(isValid)")
// Encrypt with ChaCha20-Poly1305
let key = ChaCha20Poly1305.generateKey()
let cipher = ChaCha20Poly1305(key: key)
let plaintext = "Secret message".data(using: .utf8)!
let nonce = ChaCha20Poly1305.generateNonce()
let encrypted = try cipher.encrypt(plaintext, nonce: nonce)
// Decrypt
let decrypted = try cipher.decrypt(encrypted.ciphertext, tag: encrypted.tag, nonce: nonce)
print("Decrypted: \(String(data: decrypted, encoding: .utf8)!)")
iOS/macOS Integration
Keychain Storage
import MetaMUICrypto
import Security
extension Ed25519.Keypair {
func saveToKeychain(identifier: String) throws {
// Save private key
let privateKeyData = privateKey.rawRepresentation
let privateQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "\(identifier).private".data(using: .utf8)!,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecValueData as String: privateKeyData,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
var status = SecItemAdd(privateQuery as CFDictionary, nil)
if status == errSecDuplicateItem {
SecItemDelete(privateQuery as CFDictionary)
status = SecItemAdd(privateQuery as CFDictionary, nil)
}
guard status == errSecSuccess else {
throw KeychainError.saveFailed(status)
}
// Save public key similarly
let publicKeyData = publicKey.rawRepresentation
let publicQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "\(identifier).public".data(using: .utf8)!,
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecValueData as String: publicKeyData
]
SecItemAdd(publicQuery as CFDictionary, nil)
}
static func loadFromKeychain(identifier: String) throws -> Ed25519.Keypair {
// Load private key
let privateQuery: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "\(identifier).private".data(using: .utf8)!,
kSecReturnData as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(privateQuery as CFDictionary, &item)
guard status == errSecSuccess,
let privateKeyData = item as? Data else {
throw KeychainError.loadFailed(status)
}
let privateKey = try Ed25519.PrivateKey(rawRepresentation: privateKeyData)
return Ed25519.Keypair(privateKey: privateKey)
}
}
Biometric Authentication
import LocalAuthentication
import MetaMUICrypto
class BiometricCrypto {
private let context = LAContext()
func encryptWithBiometrics(data: Data, completion: @escaping (Result<EncryptedData, Error>) -> Void) {
let reason = "Authenticate to encrypt data"
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, error in
if success {
do {
// Generate key after authentication
let key = ChaCha20Poly1305.generateKey()
let cipher = ChaCha20Poly1305(key: key)
let nonce = ChaCha20Poly1305.generateNonce()
let encrypted = try cipher.encrypt(data, nonce: nonce)
// Store key in keychain with biometric protection
self.storeKeyWithBiometricProtection(key, identifier: "biometric_key")
completion(.success(EncryptedData(
ciphertext: encrypted.ciphertext,
tag: encrypted.tag,
nonce: nonce
)))
} catch {
completion(.failure(error))
}
} else {
completion(.failure(error ?? BiometricError.authenticationFailed))
}
}
}
}
SwiftUI Integration
Crypto View Model
import SwiftUI
import MetaMUICrypto
import Combine
@MainActor
class CryptoViewModel: ObservableObject {
@Published var publicKey: String = ""
@Published var isLoading = false
@Published var error: Error?
private var keypair: Ed25519.Keypair?
func generateKeypair() async {
isLoading = true
defer { isLoading = false }
do {
keypair = try await Task.detached {
try Ed25519.generateKeypair()
}.value
publicKey = keypair!.publicKey.rawRepresentation.base64EncodedString()
} catch {
self.error = error
}
}
func signMessage(_ message: String) async -> String? {
guard let keypair = keypair,
let messageData = message.data(using: .utf8) else {
return nil
}
do {
let signature = try await Task.detached {
try Ed25519.sign(messageData, privateKey: keypair.privateKey)
}.value
return signature.base64EncodedString()
} catch {
self.error = error
return nil
}
}
}
struct CryptoView: View {
@StateObject private var viewModel = CryptoViewModel()
@State private var message = ""
@State private var signature = ""
var body: some View {
VStack(spacing: 20) {
if viewModel.isLoading {
ProgressView()
} else {
Button("Generate Keypair") {
Task {
await viewModel.generateKeypair()
}
}
if !viewModel.publicKey.isEmpty {
Text("Public Key:")
.font(.headline)
Text(viewModel.publicKey)
.font(.system(.caption, design: .monospaced))
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(8)
TextField("Enter message", text: $message)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button("Sign Message") {
Task {
if let sig = await viewModel.signMessage(message) {
signature = sig
}
}
}
.disabled(message.isEmpty)
if !signature.isEmpty {
Text("Signature:")
.font(.headline)
Text(signature)
.font(.system(.caption, design: .monospaced))
.padding()
.background(Color.green.opacity(0.1))
.cornerRadius(8)
}
}
}
}
.padding()
.alert("Error", isPresented: .constant(viewModel.error != nil)) {
Button("OK") {
viewModel.error = nil
}
} message: {
Text(viewModel.error?.localizedDescription ?? "")
}
}
}
Async/Await Support
import MetaMUICrypto
// Async key generation
func generateKeysAsync() async throws -> Ed25519.Keypair {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global(qos: .userInitiated).async {
do {
let keypair = try Ed25519.generateKeypair()
continuation.resume(returning: keypair)
} catch {
continuation.resume(throwing: error)
}
}
}
}
// Async encryption with progress
func encryptLargeFile(at url: URL, key: Data) async throws -> URL {
let cipher = ChaCha20Poly1305(key: key)
let inputHandle = try FileHandle(forReadingFrom: url)
defer { inputHandle.closeFile() }
let outputURL = url.appendingPathExtension("encrypted")
FileManager.default.createFile(atPath: outputURL.path, contents: nil)
let outputHandle = try FileHandle(forWritingTo: outputURL)
defer { outputHandle.closeFile() }
let chunkSize = 64 * 1024 // 64KB chunks
var offset: UInt64 = 0
while true {
let chunk = try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global().async {
inputHandle.seek(toFileOffset: offset)
let data = inputHandle.readData(ofLength: chunkSize)
continuation.resume(returning: data)
}
}
if chunk.isEmpty { break }
let nonce = ChaCha20Poly1305.generateNonce()
let encrypted = try cipher.encrypt(chunk, nonce: nonce)
// Write nonce + tag + ciphertext
outputHandle.write(nonce)
outputHandle.write(encrypted.tag)
outputHandle.write(encrypted.ciphertext)
offset += UInt64(chunk.count)
}
return outputURL
}
Performance Optimization
Batch Operations
import MetaMUICrypto
extension Ed25519 {
static func batchSign(messages: [Data], privateKey: PrivateKey) throws -> [Data] {
try messages.map { message in
try sign(message, privateKey: privateKey)
}
}
static func batchVerifyParallel(
signatures: [Data],
messages: [Data],
publicKeys: [PublicKey]
) async throws -> [Bool] {
try await withThrowingTaskGroup(of: (Int, Bool).self) { group in
for (index, (signature, message, publicKey)) in zip(signatures, messages, publicKeys).enumerated() {
group.addTask {
let isValid = try Ed25519.verify(signature, message: message, publicKey: publicKey)
return (index, isValid)
}
}
var results = Array(repeating: false, count: signatures.count)
for try await (index, isValid) in group {
results[index] = isValid
}
return results
}
}
}
Memory Management
import MetaMUICrypto
class SecureData {
private var data: Data
init(_ data: Data) {
self.data = data
}
deinit {
// Clear sensitive data from memory
data.withUnsafeMutableBytes { bytes in
memset_s(bytes.baseAddress, bytes.count, 0, bytes.count)
}
}
func withData<T>(_ body: (Data) throws -> T) rethrows -> T {
try body(data)
}
}
// Usage
func processSecretKey() {
let secretKey = SecureData(ChaCha20Poly1305.generateKey())
secretKey.withData { key in
let cipher = ChaCha20Poly1305(key: key)
// Use cipher...
}
// Key is automatically cleared when SecureData is deallocated
}
Testing
XCTest Integration
import XCTest
import MetaMUICrypto
final class CryptoTests: XCTestCase {
func testEd25519SignatureRoundtrip() throws {
// Given
let keypair = try Ed25519.generateKeypair()
let message = "Test message".data(using: .utf8)!
// When
let signature = try Ed25519.sign(message, privateKey: keypair.privateKey)
// Then
XCTAssertTrue(try Ed25519.verify(signature, message: message, publicKey: keypair.publicKey))
}
func testChaCha20Poly1305Encryption() throws {
// Given
let key = ChaCha20Poly1305.generateKey()
let cipher = ChaCha20Poly1305(key: key)
let plaintext = "Secret data".data(using: .utf8)!
let nonce = ChaCha20Poly1305.generateNonce()
let aad = "metadata".data(using: .utf8)!
// When
let encrypted = try cipher.encrypt(plaintext, nonce: nonce, associatedData: aad)
let decrypted = try cipher.decrypt(encrypted.ciphertext, tag: encrypted.tag, nonce: nonce, associatedData: aad)
// Then
XCTAssertEqual(decrypted, plaintext)
}
func testPerformance() throws {
let keypair = try Ed25519.generateKeypair()
let message = Data(repeating: 0, count: 1024)
measure {
_ = try? Ed25519.sign(message, privateKey: keypair.privateKey)
}
}
}
Common Patterns
Error Handling
enum CryptoOperationError: LocalizedError {
case invalidKeySize
case encryptionFailed(underlying: Error)
case decryptionFailed(underlying: Error)
case verificationFailed
var errorDescription: String? {
switch self {
case .invalidKeySize:
return "Invalid key size provided"
case .encryptionFailed(let error):
return "Encryption failed: \(error.localizedDescription)"
case .decryptionFailed(let error):
return "Decryption failed: \(error.localizedDescription)"
case .verificationFailed:
return "Signature verification failed"
}
}
}
// Usage
func encryptSafely(data: Data, key: Data) -> Result<EncryptedData, CryptoOperationError> {
guard key.count == 32 else {
return .failure(.invalidKeySize)
}
do {
let cipher = ChaCha20Poly1305(key: key)
let nonce = ChaCha20Poly1305.generateNonce()
let encrypted = try cipher.encrypt(data, nonce: nonce)
return .success(EncryptedData(ciphertext: encrypted.ciphertext, tag: encrypted.tag, nonce: nonce))
} catch {
return .failure(.encryptionFailed(underlying: error))
}
}
Best Practices
- Use Keychain for key storage on iOS/macOS
- Enable compiler optimizations for crypto code
- Clear sensitive data from memory after use
- Use async/await for CPU-intensive operations
- Validate input sizes before crypto operations
- Handle errors gracefully with proper error types
- Test with release builds for accurate performance
Troubleshooting
Common Issues
- Keychain Access Errors
// Add entitlements for keychain access // Enable "Keychain Sharing" capability in Xcode - Performance Issues
// Use release builds for testing // Enable "Whole Module Optimization" // Profile with Instruments - Memory Warnings
// Process large files in chunks // Use autoreleasepool for loops autoreleasepool { // Crypto operations }