Back to DA FlatBuffers

Swift Integration Guide

Integrate the FlatBuffers encryption WASM module into Swift applications using WasmKit, a pure Swift WebAssembly runtime.

Why WasmKit?

Prerequisites

Installation

Add to Package.swift:

dependencies: [
    .package(url: "https://github.com/AO/WasmKit.git", from: "0.0.5")
]

Quick Start

import WasmKit
import Foundation

// Load WASM module
let wasmData = try Data(contentsOf: URL(fileURLWithPath: "flatc-encryption.wasm"))
let module = try WasmKit.parseWasm(bytes: Array(wasmData))

// Create runtime
let runtime = Runtime()
let moduleInstance = try runtime.instantiate(module: module)

// Get exports
let memory = try runtime.getExport(moduleInstance, name: "memory") as! Memory
let malloc = try runtime.getExport(moduleInstance, name: "malloc") as! Function
let free = try runtime.getExport(moduleInstance, name: "free") as! Function
let encrypt = try runtime.getExport(moduleInstance, name: "wasi_encrypt_bytes") as! Function

// Generate key and IV
var key = [UInt8](repeating: 0, count: 32)
var iv = [UInt8](repeating: 0, count: 16)
_ = SecRandomCopyBytes(kSecRandomDefault, key.count, &key)
_ = SecRandomCopyBytes(kSecRandomDefault, iv.count, &iv)

let plaintext = Array("Hello, FlatBuffers!".utf8)

// Allocate WASM memory
let keyPtr = try malloc.invoke([.i32(Int32(32))]).first!.i32
let ivPtr = try malloc.invoke([.i32(Int32(16))]).first!.i32
let dataPtr = try malloc.invoke([.i32(Int32(plaintext.count))]).first!.i32

// Write to memory
try memory.write(offset: Int(keyPtr), bytes: key)
try memory.write(offset: Int(ivPtr), bytes: iv)
try memory.write(offset: Int(dataPtr), bytes: plaintext)

// Encrypt
_ = try encrypt.invoke([
    .i32(keyPtr),
    .i32(ivPtr),
    .i32(dataPtr),
    .i32(Int32(plaintext.count))
])

// Read encrypted data
let ciphertext = try memory.read(offset: Int(dataPtr), count: plaintext.count)
print("Encrypted: \(ciphertext.map { String(format: "%02x", $0) }.joined())")

// Clean up
_ = try free.invoke([.i32(keyPtr)])
_ = try free.invoke([.i32(ivPtr)])
_ = try free.invoke([.i32(dataPtr)])

Complete Module Wrapper

import WasmKit
import Foundation
import Security

/// Key and signature sizes
public enum CryptoConstants {
    public static let aesKeySize = 32
    public static let aesIVSize = 16
    public static let sha256Size = 32

    public static let x25519PrivateKeySize = 32
    public static let x25519PublicKeySize = 32

    public static let secp256k1PrivateKeySize = 32
    public static let secp256k1PublicKeySize = 33
    public static let secp256k1SignatureMaxSize = 72

    public static let ed25519PrivateKeySize = 64
    public static let ed25519PublicKeySize = 32
    public static let ed25519SignatureSize = 64
}

/// Encryption errors
public enum EncryptionError: Error, LocalizedError {
    case moduleLoadFailed(String)
    case exportNotFound(String)
    case allocationFailed
    case encryptionFailed
    case decryptionFailed
    case keyGenerationFailed
    case signatureFailed
    case verificationFailed
    case invalidKeySize(expected: Int, actual: Int)

    public var errorDescription: String? {
        switch self {
        case .moduleLoadFailed(let msg): return "Module load failed: \(msg)"
        case .exportNotFound(let name): return "Export not found: \(name)"
        case .allocationFailed: return "Memory allocation failed"
        case .encryptionFailed: return "Encryption operation failed"
        case .decryptionFailed: return "Decryption operation failed"
        case .keyGenerationFailed: return "Key generation failed"
        case .signatureFailed: return "Signature operation failed"
        case .verificationFailed: return "Verification operation failed"
        case .invalidKeySize(let expected, let actual):
            return "Invalid key size: expected \(expected), got \(actual)"
        }
    }
}

/// X25519 key pair
public struct X25519KeyPair {
    public let privateKey: [UInt8]  // 32 bytes
    public let publicKey: [UInt8]   // 32 bytes
}

/// Ed25519 key pair
public struct Ed25519KeyPair {
    public let privateKey: [UInt8]  // 64 bytes (seed + public key)
    public let publicKey: [UInt8]   // 32 bytes
}

/// secp256k1 key pair
public struct Secp256k1KeyPair {
    public let privateKey: [UInt8]  // 32 bytes
    public let publicKey: [UInt8]   // 33 bytes (compressed)
}

/// FlatBuffers Encryption Module for Swift
public final class EncryptionModule {
    private let runtime: Runtime
    private let moduleInstance: ModuleInstance
    private let memory: Memory

    private let mallocFn: Function
    private let freeFn: Function

    private let encryptFn: Function
    private let decryptFn: Function
    private let sha256Fn: Function
    private let hkdfFn: Function

    private let x25519GenerateFn: Function
    private let x25519SharedFn: Function

    private let secp256k1GenerateFn: Function
    private let secp256k1SharedFn: Function
    private let secp256k1SignFn: Function
    private let secp256k1VerifyFn: Function

    private let ed25519GenerateFn: Function
    private let ed25519SignFn: Function
    private let ed25519VerifyFn: Function

    /// Initialize from file path
    public convenience init(path: String) throws {
        let data = try Data(contentsOf: URL(fileURLWithPath: path))
        try self.init(wasmData: data)
    }

    /// Initialize from data
    public init(wasmData: Data) throws {
        let module = try WasmKit.parseWasm(bytes: Array(wasmData))

        runtime = Runtime()

        // Register WASI stubs
        try registerWasiStubs(runtime: runtime)
        try registerEnvStubs(runtime: runtime)

        moduleInstance = try runtime.instantiate(module: module)
        memory = try getExport("memory")

        mallocFn = try getExport("malloc")
        freeFn = try getExport("free")

        encryptFn = try getExport("wasi_encrypt_bytes")
        decryptFn = try getExport("wasi_decrypt_bytes")
        sha256Fn = try getExport("wasi_sha256")
        hkdfFn = try getExport("wasi_hkdf")

        x25519GenerateFn = try getExport("wasi_x25519_generate_keypair")
        x25519SharedFn = try getExport("wasi_x25519_shared_secret")

        secp256k1GenerateFn = try getExport("wasi_secp256k1_generate_keypair")
        secp256k1SharedFn = try getExport("wasi_secp256k1_shared_secret")
        secp256k1SignFn = try getExport("wasi_secp256k1_sign")
        secp256k1VerifyFn = try getExport("wasi_secp256k1_verify")

        ed25519GenerateFn = try getExport("wasi_ed25519_generate_keypair")
        ed25519SignFn = try getExport("wasi_ed25519_sign")
        ed25519VerifyFn = try getExport("wasi_ed25519_verify")
    }

    private func getExport<T>(_ name: String) throws -> T {
        guard let export = try runtime.getExport(moduleInstance, name: name) as? T else {
            throw EncryptionError.exportNotFound(name)
        }
        return export
    }

    // MARK: - Memory Management

    private func allocate(_ size: Int) throws -> Int32 {
        let result = try mallocFn.invoke([.i32(Int32(size))])
        let ptr = result.first!.i32
        if ptr == 0 {
            throw EncryptionError.allocationFailed
        }
        return ptr
    }

    private func free(_ ptr: Int32) throws {
        _ = try freeFn.invoke([.i32(ptr)])
    }

    private func writeBytes(_ ptr: Int32, _ data: [UInt8]) throws {
        try memory.write(offset: Int(ptr), bytes: data)
    }

    private func readBytes(_ ptr: Int32, _ length: Int) throws -> [UInt8] {
        return try memory.read(offset: Int(ptr), count: length)
    }

    // MARK: - Symmetric Encryption

    /// Encrypt data using AES-256-CTR
    public func encrypt(key: [UInt8], iv: [UInt8], data: [UInt8]) throws -> [UInt8] {
        guard key.count == CryptoConstants.aesKeySize else {
            throw EncryptionError.invalidKeySize(
                expected: CryptoConstants.aesKeySize,
                actual: key.count
            )
        }
        guard iv.count == CryptoConstants.aesIVSize else {
            throw EncryptionError.invalidKeySize(
                expected: CryptoConstants.aesIVSize,
                actual: iv.count
            )
        }

        let keyPtr = try allocate(key.count)
        let ivPtr = try allocate(iv.count)
        let dataPtr = try allocate(data.count)

        defer {
            try? free(keyPtr)
            try? free(ivPtr)
            try? free(dataPtr)
        }

        try writeBytes(keyPtr, key)
        try writeBytes(ivPtr, iv)
        try writeBytes(dataPtr, data)

        let result = try encryptFn.invoke([
            .i32(keyPtr),
            .i32(ivPtr),
            .i32(dataPtr),
            .i32(Int32(data.count))
        ])

        if result.first?.i32 != 0 {
            throw EncryptionError.encryptionFailed
        }

        return try readBytes(dataPtr, data.count)
    }

    /// Decrypt data using AES-256-CTR
    public func decrypt(key: [UInt8], iv: [UInt8], data: [UInt8]) throws -> [UInt8] {
        // CTR mode is symmetric
        return try encrypt(key: key, iv: iv, data: data)
    }

    // MARK: - Hash Functions

    /// Compute SHA-256 hash
    public func sha256(_ data: [UInt8]) throws -> [UInt8] {
        let dataPtr = try allocate(data.count)
        let outPtr = try allocate(CryptoConstants.sha256Size)

        defer {
            try? free(dataPtr)
            try? free(outPtr)
        }

        try writeBytes(dataPtr, data)
        _ = try sha256Fn.invoke([
            .i32(dataPtr),
            .i32(Int32(data.count)),
            .i32(outPtr)
        ])

        return try readBytes(outPtr, CryptoConstants.sha256Size)
    }

    /// Derive key using HKDF-SHA256
    public func hkdf(ikm: [UInt8], salt: [UInt8]?, info: [UInt8], length: Int) throws -> [UInt8] {
        let ikmPtr = try allocate(ikm.count)
        try writeBytes(ikmPtr, ikm)

        var saltPtr: Int32 = 0
        var saltLen = 0
        if let salt = salt, !salt.isEmpty {
            saltPtr = try allocate(salt.count)
            try writeBytes(saltPtr, salt)
            saltLen = salt.count
        }

        let infoPtr = try allocate(info.count)
        try writeBytes(infoPtr, info)

        let outPtr = try allocate(length)

        defer {
            try? free(ikmPtr)
            if saltPtr != 0 { try? free(saltPtr) }
            try? free(infoPtr)
            try? free(outPtr)
        }

        _ = try hkdfFn.invoke([
            .i32(ikmPtr), .i32(Int32(ikm.count)),
            .i32(saltPtr), .i32(Int32(saltLen)),
            .i32(infoPtr), .i32(Int32(info.count)),
            .i32(outPtr), .i32(Int32(length))
        ])

        return try readBytes(outPtr, length)
    }

    // MARK: - X25519 Key Exchange

    /// Generate X25519 key pair
    public func x25519GenerateKeyPair() throws -> X25519KeyPair {
        let privPtr = try allocate(CryptoConstants.x25519PrivateKeySize)
        let pubPtr = try allocate(CryptoConstants.x25519PublicKeySize)

        defer {
            try? free(privPtr)
            try? free(pubPtr)
        }

        let result = try x25519GenerateFn.invoke([.i32(privPtr), .i32(pubPtr)])

        if result.first?.i32 != 0 {
            throw EncryptionError.keyGenerationFailed
        }

        return X25519KeyPair(
            privateKey: try readBytes(privPtr, CryptoConstants.x25519PrivateKeySize),
            publicKey: try readBytes(pubPtr, CryptoConstants.x25519PublicKeySize)
        )
    }

    /// Compute X25519 shared secret
    public func x25519SharedSecret(privateKey: [UInt8], publicKey: [UInt8]) throws -> [UInt8] {
        let privPtr = try allocate(privateKey.count)
        let pubPtr = try allocate(publicKey.count)
        let outPtr = try allocate(32)

        defer {
            try? free(privPtr)
            try? free(pubPtr)
            try? free(outPtr)
        }

        try writeBytes(privPtr, privateKey)
        try writeBytes(pubPtr, publicKey)

        let result = try x25519SharedFn.invoke([
            .i32(privPtr),
            .i32(pubPtr),
            .i32(outPtr)
        ])

        if result.first?.i32 != 0 {
            throw EncryptionError.encryptionFailed
        }

        return try readBytes(outPtr, 32)
    }

    // MARK: - Ed25519 Signatures

    /// Generate Ed25519 key pair
    public func ed25519GenerateKeyPair() throws -> Ed25519KeyPair {
        let privPtr = try allocate(CryptoConstants.ed25519PrivateKeySize)
        let pubPtr = try allocate(CryptoConstants.ed25519PublicKeySize)

        defer {
            try? free(privPtr)
            try? free(pubPtr)
        }

        let result = try ed25519GenerateFn.invoke([.i32(privPtr), .i32(pubPtr)])

        if result.first?.i32 != 0 {
            throw EncryptionError.keyGenerationFailed
        }

        return Ed25519KeyPair(
            privateKey: try readBytes(privPtr, CryptoConstants.ed25519PrivateKeySize),
            publicKey: try readBytes(pubPtr, CryptoConstants.ed25519PublicKeySize)
        )
    }

    /// Sign with Ed25519
    public func ed25519Sign(privateKey: [UInt8], message: [UInt8]) throws -> [UInt8] {
        let privPtr = try allocate(privateKey.count)
        let msgPtr = try allocate(message.count)
        let sigPtr = try allocate(CryptoConstants.ed25519SignatureSize)

        defer {
            try? free(privPtr)
            try? free(msgPtr)
            try? free(sigPtr)
        }

        try writeBytes(privPtr, privateKey)
        try writeBytes(msgPtr, message)

        let result = try ed25519SignFn.invoke([
            .i32(privPtr),
            .i32(msgPtr),
            .i32(Int32(message.count)),
            .i32(sigPtr)
        ])

        if result.first?.i32 != 0 {
            throw EncryptionError.signatureFailed
        }

        return try readBytes(sigPtr, CryptoConstants.ed25519SignatureSize)
    }

    /// Verify Ed25519 signature
    public func ed25519Verify(publicKey: [UInt8], message: [UInt8], signature: [UInt8]) throws -> Bool {
        let pubPtr = try allocate(publicKey.count)
        let msgPtr = try allocate(message.count)
        let sigPtr = try allocate(signature.count)

        defer {
            try? free(pubPtr)
            try? free(msgPtr)
            try? free(sigPtr)
        }

        try writeBytes(pubPtr, publicKey)
        try writeBytes(msgPtr, message)
        try writeBytes(sigPtr, signature)

        let result = try ed25519VerifyFn.invoke([
            .i32(pubPtr),
            .i32(msgPtr),
            .i32(Int32(message.count)),
            .i32(sigPtr)
        ])

        return result.first?.i32 == 0
    }
}

// MARK: - WASI Stubs

private func registerWasiStubs(runtime: Runtime) throws {
    try runtime.defineHostFunction(
        module: "wasi_snapshot_preview1",
        name: "fd_close",
        paramTypes: [.i32],
        resultTypes: [.i32]
    ) { _ in [.i32(0)] }

    try runtime.defineHostFunction(
        module: "wasi_snapshot_preview1",
        name: "clock_time_get",
        paramTypes: [.i32, .i64, .i32],
        resultTypes: [.i32]
    ) { _ in [.i32(0)] }

    // Add other WASI stubs as needed...
}

private func registerEnvStubs(runtime: Runtime) throws {
    try runtime.defineHostFunction(
        module: "env",
        name: "invoke_v",
        paramTypes: [.i32],
        resultTypes: []
    ) { _ in [] }

    try runtime.defineHostFunction(
        module: "env",
        name: "invoke_vi",
        paramTypes: [.i32, .i32],
        resultTypes: []
    ) { _ in [] }

    // Add other invoke_* stubs as needed...
}

Template Project Structure

MyProject/
├── Package.swift
├── Sources/
│   └── MyProject/
│       ├── main.swift
│       └── Encryption/
│           └── EncryptionModule.swift
├── Resources/
│   └── flatc-encryption.wasm
└── Tests/
    └── MyProjectTests/
        └── EncryptionTests.swift

Package.swift:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyProject",
    platforms: [.macOS(.v13), .iOS(.v16)],
    dependencies: [
        .package(url: "https://github.com/AO/WasmKit.git", from: "0.0.5")
    ],
    targets: [
        .executableTarget(
            name: "MyProject",
            dependencies: ["WasmKit"],
            resources: [.copy("Resources/flatc-encryption.wasm")]
        ),
        .testTarget(
            name: "MyProjectTests",
            dependencies: ["MyProject"]
        )
    ]
)

Usage Examples

Basic Encryption

let module = try EncryptionModule(path: "flatc-encryption.wasm")

var key = [UInt8](repeating: 0, count: 32)
var iv = [UInt8](repeating: 0, count: 16)
_ = SecRandomCopyBytes(kSecRandomDefault, key.count, &key)
_ = SecRandomCopyBytes(kSecRandomDefault, iv.count, &iv)

let plaintext = Array("Secret message".utf8)
let ciphertext = try module.encrypt(key: key, iv: iv, data: plaintext)
let decrypted = try module.decrypt(key: key, iv: iv, data: ciphertext)

assert(plaintext == decrypted)

End-to-End Encryption

let module = try EncryptionModule(path: "flatc-encryption.wasm")

// Generate key pairs
let alice = try module.x25519GenerateKeyPair()
let bob = try module.x25519GenerateKeyPair()

// Compute shared secrets
let aliceShared = try module.x25519SharedSecret(
    privateKey: alice.privateKey,
    publicKey: bob.publicKey
)
let bobShared = try module.x25519SharedSecret(
    privateKey: bob.privateKey,
    publicKey: alice.publicKey
)

assert(aliceShared == bobShared)

// Derive encryption key
let encryptionKey = try module.hkdf(
    ikm: aliceShared,
    salt: nil,
    info: Array("encryption-v1".utf8),
    length: 32
)

// Encrypt
var iv = [UInt8](repeating: 0, count: 16)
_ = SecRandomCopyBytes(kSecRandomDefault, iv.count, &iv)
let ciphertext = try module.encrypt(key: encryptionKey, iv: iv, data: Array("Hello Bob!".utf8))

// Decrypt
let decrypted = try module.decrypt(key: encryptionKey, iv: iv, data: ciphertext)
print(String(bytes: decrypted, encoding: .utf8)!) // "Hello Bob!"

Performance Tips

  1. Reuse module instances - Module loading is expensive
  2. Use async/await - For non-blocking operations in SwiftUI
  3. Batch allocations - Minimize malloc/free calls

Troubleshooting

"Export not found"

Ensure WASI and env stubs are registered before instantiation.

"Memory access out of bounds"

Check allocation succeeded and sizes are correct.

See Also