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
Swift Package Manager
Add MetaMUI to 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(url: "https://github.com/metamui/crypto-swift.git", from: "0.1.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "MetaMUICrypto", package: "crypto-swift")
]
)
]
)
Xcode Integration
- File → Add Package Dependencies
- Enter:
https://github.com/metamui/crypto-swift.git - Choose version rule: “Up to Next Major Version”
- Select the algorithms you need
CocoaPods
pod 'MetaMUICrypto', '~> 0.1.0'
# Or specific algorithms
pod 'MetaMUICrypto/Ed25519'
pod 'MetaMUICrypto/ChaCha20'
pod 'MetaMUICrypto/PostQuantum'
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
}
Combine Integration
import Combine
import MetaMUICrypto
class CryptoService {
enum CryptoError: Error {
case encryptionFailed
case decryptionFailed
case keyGenerationFailed
}
func generateKeypairPublisher() -> AnyPublisher<Ed25519.Keypair, Error> {
Future { promise in
DispatchQueue.global(qos: .userInitiated).async {
do {
let keypair = try Ed25519.generateKeypair()
promise(.success(keypair))
} catch {
promise(.failure(CryptoError.keyGenerationFailed))
}
}
}
.eraseToAnyPublisher()
}
func encryptPublisher(data: Data, key: Data) -> AnyPublisher<EncryptedData, Error> {
Future { promise in
do {
let cipher = ChaCha20Poly1305(key: key)
let nonce = ChaCha20Poly1305.generateNonce()
let encrypted = try cipher.encrypt(data, nonce: nonce)
promise(.success(EncryptedData(
ciphertext: encrypted.ciphertext,
tag: encrypted.tag,
nonce: nonce
)))
} catch {
promise(.failure(CryptoError.encryptionFailed))
}
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.eraseToAnyPublisher()
}
}
// Usage with Combine
class ViewModel: ObservableObject {
@Published var encryptedData: EncryptedData?
private var cancellables = Set<AnyCancellable>()
private let cryptoService = CryptoService()
func encryptMessage(_ message: String) {
guard let data = message.data(using: .utf8) else { return }
let key = ChaCha20Poly1305.generateKey()
cryptoService.encryptPublisher(data: data, key: key)
.sink(
receiveCompletion: { completion in
if case .failure(let error) = completion {
print("Encryption failed: \(error)")
}
},
receiveValue: { [weak self] encrypted in
self?.encryptedData = encrypted
}
)
.store(in: &cancellables)
}
}
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)
}
}
}
Test Utilities
extension Data {
static func random(count: Int) -> Data {
var data = Data(count: count)
_ = data.withUnsafeMutableBytes { bytes in
SecRandomCopyBytes(kSecRandomDefault, count, bytes.baseAddress!)
}
return data
}
}
extension XCTestCase {
func assertCryptoEqual(_ expression1: @autoclosure () throws -> Data,
_ expression2: @autoclosure () throws -> Data,
file: StaticString = #filePath,
line: UInt = #line) {
do {
let data1 = try expression1()
let data2 = try expression2()
// Constant-time comparison
guard data1.count == data2.count else {
XCTFail("Data lengths don't match", file: file, line: line)
return
}
var equal = true
for (byte1, byte2) in zip(data1, data2) {
equal = equal && (byte1 == byte2)
}
XCTAssertTrue(equal, "Crypto data not equal", file: file, line: line)
} catch {
XCTFail("Threw error: \(error)", file: file, line: line)
}
}
}
Common Patterns
Protocol-Oriented Crypto
protocol CryptoProvider {
associatedtype PublicKey
associatedtype PrivateKey
associatedtype Signature
static func generateKeypair() throws -> (PublicKey, PrivateKey)
static func sign(_ message: Data, privateKey: PrivateKey) throws -> Signature
static func verify(_ signature: Signature, message: Data, publicKey: PublicKey) throws -> Bool
}
extension Ed25519: CryptoProvider {
typealias Signature = Data
}
// Generic crypto operations
func performCryptoOperation<T: CryptoProvider>(using provider: T.Type, message: Data) throws {
let (publicKey, privateKey) = try provider.generateKeypair()
let signature = try provider.sign(message, privateKey: privateKey)
let isValid = try provider.verify(signature, message: message, publicKey: publicKey)
print("Signature valid: \(isValid)")
}
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 }