Swift is the language Apple wants you to write everything on Apple platforms in. Operators run into it most often as the language of the target side: macOS productivity apps, iOS implants and jailbreaks, the increasingly large slice of the iOS app ecosystem that’s been migrated off Objective-C. Less often as the language of operator-host tooling, because Python is faster to write and runs on everything. The question for the operator who’s never written Swift before is whether it’s worth learning, and the answer is: yes if you’re going to do macOS or iOS work, mostly no otherwise, but the syntax is pleasant enough that picking it up takes a weekend either way.
This walkthrough covers enough of Swift’s syntax and idioms to read other people’s code (the operator-relevant skill), then the offensive applications worth knowing on Apple platforms: cryptographic primitives via CommonCrypto and SwiftCrypto, network scanning via the Network framework, file-system reconnaissance, OSINT-style API consumption, basic listener/echo servers for honeypot or analysis work, and payload encoding. The closing section weighs Swift against Python, Go, and Rust for offensive use in 2026.
Language background#
Swift was announced at WWDC on June 2, 2014, with development started by Chris Lattner (the original author of LLVM and Clang) at Apple in 2010. The pitch was straightforward: Objective-C was old, ugly, and had a syntax that scared off new iOS developers; Apple wanted a modern language they fully controlled. Swift went open-source under the Apache 2.0 license on December 3, 2015. The current stable as of 2026 is Swift 6.4 (released March 2026), with strict concurrency checking as the default since Swift 6.0 in 2024.
Swift runs natively on macOS, iOS, watchOS, tvOS, and visionOS as you’d expect, plus officially on Linux (since Swift 2 in 2015) and Windows (since Swift 5.3 in September 2020, via Saleem Abdulrasool’s work as the Windows platform champion on the Swift Core Team, not via the abandoned Swift for TensorFlow project that some older posts incorrectly cite). Cross-platform Swift works but is meaningfully less polished than Apple-platform Swift; libraries built around the Foundation framework (URLSession, FileManager, Date) work on all platforms, while anything in the Network framework is Apple-only and needs an alternative (SwiftNIO) on Linux. For crypto, the modern Apple API is CryptoKit; SwiftCrypto re-exports it on Apple and provides a BoringSSL-backed implementation on Linux and ARM64 Windows.
Variables and types#
Swift distinguishes mutable variables from immutable constants at the language level, and the convention is to default to constants unless mutation is genuinely needed. var for the mutable case, let for the immutable case.
var userScore = 0
userScore = 100 // legal
let targetIP = "192.168.1.1"
// targetIP = "10.0.0.1" // compile errorSwift is strongly typed but uses type inference for most declarations. You only write explicit type annotations when the compiler can’t deduce the type (typically: empty collections, function signatures, or when you want a wider type than the initializer suggests):
var inferredInt = 10 // Int
let inferredString = "Swift" // String
let inferredBool = true // Bool
var explicitInt: Int = 42
let explicitDouble: Double = 3.141592
var emptyArray: [String] = [] // type annotation required for empty
var optionalString: String? // optional, more on this belowPrimitive types#
The core numeric and string types are what you’d expect from any modern language:
IntandUInt: signed and unsigned integers sized to the platform’s native word width (64-bit on every system anyone is targeting in 2026). Sized variantsInt8,Int16,Int32,Int64,UInt8…UInt64exist for specific bit-width work (binary protocols, fixed-format file headers, struct layouts that have to match a C library).DoubleandFloat: 64-bit and 32-bit IEEE 754 floats.Doubleis the default.Bool:trueorfalse. No truthiness coercion; conditionals requireBool.String: Unicode-correct strings backed by UTF-8 storage. Operations are slower than C-style byte arrays because the type does the right thing with grapheme clusters; for performance-critical byte work, drop toDataor[UInt8].Character: one extended grapheme cluster. AStringis conceptually a sequence ofCharacters, though it’s stored as UTF-8.
Collections#
var ipAddresses: [String] = ["192.168.1.1", "192.168.1.10"]
ipAddresses.append("192.168.1.50")
var portServices: [Int: String] = [
22: "SSH",
80: "HTTP",
443: "HTTPS",
]
portServices[3389] = "RDP"
var findings: Set<String> = ["SQLi", "XSS", "CSRF"]
findings.insert("SQLi") // no-op; sets dedupe
findings.insert("Buffer Overflow")Three collection types: [Element] for ordered arrays, [Key: Value] for dictionaries, Set<Element> for unordered unique-value sets. All three are value types (copy on assignment), implemented as copy-on-write under the hood so the copy is cheap until you mutate.
Operators#
Standard C-family operators, with a few Swift-specific behaviors:
// Arithmetic: + - * / % (integer / truncates; use Float/Double for fractional)
let sum = 10 + 3 // 13
let quot = 10 / 3 // 3
let rem = 10 % 3 // 1
// Comparison: == != < > <= >=
let isEqual = 5 == 10 // false
// Logical: ! && || (Bool operands only; no truthiness coercion)
let hasAccess = isLoggedIn && isAdmin
// Bitwise: ~ & | ^ << >>
let mask = 0b1100 & 0b1010 // 0b1000 (8)
let shift = 0b1100 << 1 // 0b11000 (24)Two Swift-specific quirks: integer overflow traps at runtime by default (use the &+, &-, &* variants if you actually want wrapping arithmetic), and there’s no implicit numeric conversion (Int(myFloat) is required to convert between numeric types). Both are safety features that cost a small amount of verbosity in exchange for catching the kinds of bugs that ship to production in C.
Control flow#
Conditionals with if/else, switch/case, ranges (1...5 inclusive, 1..<5 half-open) for iteration:
if temperature > 20 {
print("It's hot")
} else {
print("It's not")
}
switch grade {
case "A": print("Excellent")
case "B": print("Good")
case "C", "D": print("Acceptable")
case let other: print("Invalid: \(other)") // pattern binding
}
for i in 1...5 { // closed range, 1 through 5
print(i)
}
var counter = 0
while counter < 5 {
counter += 1
}
repeat { // do-while in other languages
counter -= 1
} while counter > 0Switch in Swift is exhaustive (the compiler enforces that you cover every case for enum-typed switches, or include a default) and supports pattern matching on tuples, ranges, and associated values. There’s no fall-through; cases don’t need an explicit break.
Functions#
func greet() {
print("Hello")
}
func greet(name: String) {
print("Hello, \(name)")
}
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
greet()
greet(name: "Alice") // argument labels are required by default
let sum = add(5, 3) // the `_` in the signature suppresses labelsSwift requires argument labels at the call site unless you suppress them with _ in the function signature. This is the opposite of most C-family languages; it makes call sites more readable at the cost of more typing. Functions can return tuples for multiple values, take closures as arguments, and be generic over types.
Advanced concepts#
Past the basics, the Swift features the operator should know:
Optionals#
Optionals are Swift’s answer to the null-pointer problem. Any type that might lack a value is declared with ?, which produces a wrapper type the compiler forces you to unwrap before using. This catches an entire class of bugs at compile time that haunt C, Objective-C, and the older Java code base.
var optionalString: String? = "Hello"
var optionalInt: Int? = nilThe standard ways to safely access an optional’s value are if let, guard let, and the nil-coalescing operator ??:
if let s = optionalString {
print("got: \(s)")
} else {
print("was nil")
}
func process(_ value: String?) {
guard let v = value else {
return // early-exit pattern
}
print("processing: \(v)")
}
let display = optionalInt ?? -1 // default if nil
let forced = optionalString! // force unwrap; crashes if nilThe force-unwrap ! exists for the rare cases where you can prove the value is non-nil but the type system can’t. In practice, most experienced Swift code avoids it entirely outside of IBOutlet declarations and a handful of internal-invariant cases, because every ! is a potential crash site.
Error handling#
Errors are types conforming to the Error protocol; functions that can fail are marked throws and call sites use try, with do/catch blocks for handling.
enum NetworkError: Error {
case invalidURL
case noData
case serverError(code: Int)
}
func fetchData(from urlString: String) throws -> Data {
guard let url = URL(string: urlString) else {
throw NetworkError.invalidURL
}
// ... actual fetch ...
return Data()
}
do {
let data = try fetchData(from: "https://example.com")
print("got \(data.count) bytes")
} catch NetworkError.invalidURL {
print("bad url")
} catch NetworkError.serverError(let code) {
print("server error \(code)")
} catch {
print("unexpected: \(error)") // bound to local `error` variable
}
let optionalData = try? fetchData(from: url) // converts throws to optional
// let crashing = try! fetchData(...) // crashes on errorErrors are values, not exceptions in the C++/Java sense. They unwind through throws declarations in the type signature, and the compiler enforces that every try is either inside a do/catch or in another throws function. The result is more verbose than Python’s try/except but the compile-time checking catches the “we forgot to handle this error” case before it ships.
Structs vs classes#
Two ways to define custom types, distinguished by value-versus-reference semantics:
structis a value type. Assignment and parameter passing copy the value. Modifications to the copy don’t affect the original. Stored on the stack when the value is small enough to fit; copy-on-write for collections and large payloads.classis a reference type. Assignment and parameter passing copy the reference, not the underlying object. Two variables holding the same class instance see the same mutations. Always heap-allocated. Supports inheritance, deinitializers, and Objective-C interop.
struct NetworkDevice {
var ip: String
var openPorts: [Int]
}
var a = NetworkDevice(ip: "192.168.1.1", openPorts: [80, 443])
var b = a // copy
b.openPorts.append(22)
print(a.openPorts) // [80, 443] -- unchanged
print(b.openPorts) // [80, 443, 22]
class ExploitModule {
var version: String
init(version: String) { self.version = version }
}
let x = ExploitModule(version: "1.0")
let y = x // reference
y.version = "1.1"
print(x.version) // "1.1" -- both see the changeThe modern Swift idiom is “structs by default, classes when you actually need reference semantics or inheritance.” The Foundation types you’ll use most often (String, Array, Dictionary, Set, Data) are all value types under the hood; the small handful of reference types (URLSession, FileManager, NSRegularExpression) are usually wrappers around shared system resources.
Protocols#
Protocols are Swift’s interface type: a contract that says “any type conforming to this protocol provides these methods and properties.” Conformance can be retroactively added by extensions, which makes the design more flexible than Java-style inheritance hierarchies.
protocol Scanner {
var target: String { get set }
func performScan() throws -> String
}
struct PortScanner: Scanner {
var target: String
var ports: ClosedRange<Int>
func performScan() throws -> String {
// ... real scan logic ...
return "Open ports on \(target): 22, 80"
}
}
struct VulnScanner: Scanner {
var target: String
var vulnDB: [String]
func performScan() throws -> String {
return vulnDB.isEmpty
? "no findings"
: "findings: \(vulnDB.joined(separator: ", "))"
}
}Extensions#
Extensions add new functionality to existing types without modifying the original source. The reach is broad: you can add methods to Apple’s own String and Data, retroactively conform Foundation types to your own protocols, and split a complex type’s implementation across multiple files for organization. This is the feature that makes Swift collections feel as natural as they do; most of what you can do with a String is actually defined in extensions in different parts of the standard library.
import Foundation
import CryptoKit // Apple-platform modern crypto API
// Use `import Crypto` on Linux via swift-crypto
extension String {
func sha256() -> String {
let digest = SHA256.hash(data: Data(self.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
}
func base64Encoded() -> String? {
return Data(self.utf8).base64EncodedString()
}
func base64Decoded() -> String? {
guard let data = Data(base64Encoded: self) else { return nil }
return String(data: data, encoding: .utf8)
}
}
print("admin".sha256()) // 8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918
print("Hello World!".base64Encoded() ?? "")The older CommonCrypto C framework still works but has the awkward C-style API. CryptoKit (introduced in iOS 13 / macOS 10.15) is the modern Swift-native equivalent and is the right default for new code.
Concurrency#
Swift’s async/await landed in Swift 5.5 (September 2021), with actor types in the same release for safe shared-state work. Swift 6 (2024) made strict concurrency checking the default, which means the compiler now enforces data-race freedom at compile time across most code paths, which is a meaningful upgrade for the kinds of network and file-system tools the operator builds.
import Foundation
func fetch(_ host: String) async throws -> String {
try await Task.sleep(nanoseconds: 1_000_000_000) // 1 second
return "data from \(host)"
}
func runConcurrent() async {
async let a = fetch("host1.com")
async let b = fetch("host2.com")
async let c = fetch("host3.com")
do {
let results = try await [a, b, c] // all three run concurrently
for r in results { print(r) }
} catch {
print("error: \(error)")
}
}
actor LogCollector {
private var logs: [String] = []
func add(_ message: String) { logs.append(message) }
func all() -> [String] { logs }
}
func demoActor() async {
let collector = LogCollector()
await collector.add("scan started")
await collector.add("found port 80")
let logs = await collector.all()
for log in logs { print(log) }
}
Task { await runConcurrent() }
Task { await demoActor() }async let runs the right-hand side concurrently with surrounding code, awaiting only when the value is needed. Actors serialize access to their mutable state, so the compiler can prove that no two threads ever mutate logs at the same time. Both features fundamentally change how you write Swift networking code; for the operator, they make concurrent port scanning and parallel API calls a few lines instead of a DispatchQueue setup.
Memory management (ARC)#
Swift uses Automatic Reference Counting (ARC) for class instances. When the strong reference count drops to zero, the instance deallocates. No GC pause, no malloc/free. The thing to know: strong-reference cycles between class instances leak memory, and you break them with weak (optional, becomes nil when the target deallocates) or unowned (non-optional, crashes if the target is gone; use only when you can prove the relationship outlives the reference).
Value types (structs, enums, tuples) don’t participate in ARC because they don’t have reference semantics; they’re copied or moved as scalars and freed when their scope ends.
Building from the command line#
The minimal flow without Xcode:
# Install Swift via Apple's installer, or rustup-style toolchain manager `swiftly`
# https://www.swift.org/install/
# One-file script
echo 'print("Hello, World!")' > main.swift
swiftc main.swift -o hello
./hello
# Real project with dependencies (the modern default)
mkdir tool && cd tool
swift package init --type executable
swift build
swift runSwift Package Manager (SPM) is Apple’s official build system and dependency manager, introduced with Swift 3 in 2016 and the de facto standard for cross-platform Swift in 2026. Package.swift declares dependencies in Swift itself (the manifest is executable code); swift build and swift run work on macOS, Linux, and Windows without IDE involvement. For Apple-platform GUI work you’ll still want Xcode, but for operator tooling that runs as a CLI, SPM is enough.
Swift on engagement#
The offensive work where Swift earns its place is on Apple targets: macOS post-exploitation tooling, iOS implants, anything that needs to run inside the Apple sandbox model without tripping notarization or hardened runtime alarms. Below are the patterns that come up most often.
Brute-force hash cracking#
A worked example of CryptoKit, string manipulation, and recursion. The technique is straightforward (try every charset combination up to a length, hash each, compare), so it’s good as a Swift-syntax demonstration even if you’d never use it against real passwords (any modern attacker reaches for Hashcat or John on GPU instead of brute-forcing serially on a CPU).
import Foundation
import CryptoKit
extension Data {
func sha256() -> Data {
return Data(SHA256.hash(data: self))
}
var hex: String {
return map { String(format: "%02x", $0) }.joined()
}
}
func crack(target: String, charset: String, maxLen: Int) -> String? {
func recurse(_ attempt: String) -> String? {
if Data(attempt.utf8).sha256().hex == target {
return attempt
}
if attempt.count >= maxLen {
return nil
}
for ch in charset {
if let result = recurse(attempt + String(ch)) {
return result
}
}
return nil
}
return recurse("")
}
let charset = "abcdefghijklmnopqrstuvwxyz0123456789"
// SHA256 of "abc"
let target = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
if let cracked = crack(target: target, charset: charset, maxLen: 4) {
print("found: \(cracked)")
} else {
print("not found")
}The recursive search-tree approach is clean Swift but has the same exponential blow-up as any brute-force: every character of password length multiplies the search space by the charset size. The example uses CryptoKit’s SHA256.hash() rather than the older CommonCrypto C API; CryptoKit is the right default for new Apple-platform code, and on Linux/Windows the same code works against Apple’s swift-crypto package (import Crypto instead of import CryptoKit).
Port scanning with the Network framework#
Apple’s Network framework (introduced at WWDC 2018 for iOS 12 / macOS Mojave) is the modern way to do networking on Apple platforms. It replaces BSD sockets and the older CFNetwork APIs with a connection-oriented, asynchronous design that works naturally with async/await. For port scanning specifically, the framework lets you spin up dozens of NWConnection instances concurrently without thread management.
import Foundation
import Network // Requires Network.framework, typically on Apple platforms
/// Scans a range of TCP ports on a given host.
/// - Parameters:
/// - host: The target host (IP address or hostname).
/// - ports: A closed range of ports to scan (for example, 1...1024).
/// - timeout: The maximum time in seconds to wait for a connection to establish for each port.
/// - completion: A closure that is called when the scan is completed, returning an array of open ports.
func scanPorts(host: String, ports: ClosedRange<Int>, timeout: TimeInterval = 2.0, completion: @escaping ([Int]) -> Void) {
let dispatchQueue = DispatchQueue(label: "portScannerQueue", attributes: .concurrent)
let dispatchGroup = DispatchGroup()
var openPorts = [Int]()
let lock = NSLock() // To safely append to openPorts from multiple threads
print("Starting port scan on \(host) for ports \(ports.lowerBound)-\(ports.upperBound)...")
for port in ports {
dispatchGroup.enter() // Indicate that a task has started
dispatchQueue.async {
let endpoint = NWEndpoint.Host(host)
let portEndpoint = NWEndpoint.Port(rawValue: UInt16(port))!
let connection = NWConnection(to: portEndpoint, using: .tcp)
connection.stateUpdateHandler = { state in
switch state {
case .ready:
lock.lock()
openPorts.append(port)
lock.unlock()
print("Port \(port) on \(host) is OPEN.")
connection.cancel() // Close the connection once port is found open
dispatchGroup.leave() // Indicate that this task is finished
case .failed(let error):
// print("Port \(port) on \(host) is CLOSED or filtered. Error: \(error.localizedDescription)")
connection.cancel()
dispatchGroup.leave()
case .cancelled:
dispatchGroup.leave()
default:
break
}
}
connection.start(queue: dispatchQueue)
// Set a timeout for the connection attempt
dispatchQueue.asyncAfter(deadline: .now() + timeout) {
if connection.state != .ready && connection.state != .failed && connection.state != .cancelled {
// print("Port \(port) on \(host) timed out.")
connection.cancel() // Cancel if it hasn't connected or failed yet
}
}
}
}
// Notify when all tasks in the group are complete
dispatchGroup.notify(queue: .main) {
let sortedOpenPorts = openPorts.sorted()
print("\nPort scanning completed for \(host). Found \(sortedOpenPorts.count) open ports: \(sortedOpenPorts)")
completion(sortedOpenPorts)
}
}
// --- Usage Example ---
let targetHost = "scanme.nmap.org" // Use a legitimate target for testing, like scanme.nmap.org
let targetPorts: ClosedRange<Int> = 20...100
// Call the scanPorts function
scanPorts(host: targetHost, ports: targetPorts, timeout: 3.0) { foundPorts in
print("Final list of open ports: \(foundPorts)")
}
// Note: For real-world use, ensure you have permission to scan the target host.
// The Network framework is primarily for client-side connections. For raw socket
// programming or more advanced packet manipulation, you might need to drop down
// to C-level APIs or use third-party libraries.The scanner uses NWConnection to attempt TCP connections to each port in the requested range, running connections concurrently on a dispatch queue. A DispatchGroup tracks when all attempts have completed (either by connecting, failing, or timing out), and an NSLock protects the shared openPorts array from concurrent appends. The example targets scanme.nmap.org, which the Nmap project hosts specifically as a public testing target.
For stealth scans (SYN, FIN, NULL, XMAS), the Network framework is not enough; you need raw sockets, which require root privileges on Linux/macOS and aren’t directly exposed by the modern Apple frameworks. Operators doing stealth scanning from Swift on a Mac typically bridge to C via libpcap or wrap nmap as a subprocess. For the common case of “is this TCP port open?”, the Network framework version above is enough.
File-system reconnaissance#
On a compromised macOS host, the operator’s first task is figuring out what’s on the box that matters. Browser cookies, SSH keys, AWS credentials in ~/.aws, kubeconfig files, source code in ~/repos, anything in ~/Documents that looks sensitive. Swift’s FileManager and URL types make recursive searches straightforward without shelling out.
import Foundation
/// Searches for files with a specific extension within a directory and its subdirectories.
/// - Parameters:
/// - directoryURL: The URL of the directory to start the search from.
/// - fileExtension: The file extension to search for (for example, "log", "conf", "key").
/// - Returns: An array of URLs pointing to the found files.
func findFiles(in directoryURL: URL, withExtension fileExtension: String) -> [URL] {
let fileManager = FileManager.default
var foundFiles: [URL] = []
guard let enumerator = fileManager.enumerator(at: directoryURL,
includingPropertiesForKeys: [.isRegularFileKey],
options: [.skipsHiddenFiles, .skipsPackageDescendants]) else {
print("Error: Could not create enumerator for directory: \(directoryURL.path)")
return []
}
print("Searching for *.\(fileExtension) files in \(directoryURL.path) and its subdirectories...")
for case let fileURL as URL in enumerator {
do {
let fileAttributes = try fileURL.resourceValues(forKeys: [.isRegularFileKey])
if fileAttributes.isRegularFile == true && fileURL.pathExtension == fileExtension {
foundFiles.append(fileURL)
}
} catch {
print("Error accessing file attributes for \(fileURL.path): \(error.localizedDescription)")
}
}
return foundFiles
}
// --- Usage Example ---
// IMPORTANT: Replace with a path you have permission to access and want to scan.
// For security reasons, avoid scanning sensitive system directories without explicit understanding.
let startDirectory = URL(fileURLWithPath: "/Users/youruser/Documents") // Example: Your Documents folder
let sensitiveExtension = "log"
let sensitiveFiles = findFiles(in: startDirectory, withExtension: sensitiveExtension)
if sensitiveFiles.isEmpty {
print("No .\(sensitiveExtension) files found in \(startDirectory.path).")
} else {
print("\nFound the following .\(sensitiveExtension) files:")
for file in sensitiveFiles {
print("- \(file.lastPathComponent) (\(file.path))")
}
}
// Example: Searching for SSH private keys
let sshKeyDirectory = URL(fileURLWithPath: "/Users/youruser/.ssh") // Example: Your SSH directory
let keyFiles = findFiles(in: sshKeyDirectory, withExtension: "key")
if !keyFiles.isEmpty {
print("\nPotentially sensitive key files found:")
for keyFile in keyFiles {
print("- \(keyFile.lastPathComponent) (\(keyFile.path))")
}
}The FileManager.enumerator(at:includingPropertiesForKeys:options:) API does the recursive walk for you, with options to skip hidden files and package descendants (the macOS convention where directories like .app bundles look like single files). Filtering on pathExtension finds files by suffix; for finding by name pattern, use lastPathComponent and NSRegularExpression. For content-based search (grepping every file under ~ for “AKIA” to find AWS keys), open each file with String(contentsOf:) and pattern-match.
On a real engagement, the macOS Transparency, Consent, and Control (TCC) framework will prompt for access to a lot of directories the first time your process tries to read them: ~/Documents, ~/Downloads, ~/Desktop, ~/Library/Mail, anything in iCloud Drive. If your implant doesn’t have the right entitlements (or hasn’t already been granted Full Disk Access by the user), the read will fail silently. Plan for the TCC denial; don’t assume FileManager can see everything.
TCP listener for honeypot or analysis work#
Raw packet sniffing on a network interface requires libpcap and elevated privileges, which is outside what Swift’s standard frameworks expose. What the Network framework does give you cleanly is the inverse case: standing up a TCP listener that accepts incoming connections, processes the data, and echoes or logs it. Useful as a honeypot in the lab, as the receiving end of a custom protocol you’re reverse-engineering, or as a test harness for a target you’re poking at.
import Foundation
import Network
/// A simple TCP listener that echoes received data.
class TCPEchoListener {
private let port: NWEndpoint.Port
private var listener: NWListener?
init(port: UInt16) {
self.port = NWEndpoint.Port(rawValue: port)!
}
func start() throws {
listener = try NWListener(using: .tcp, on: port)
listener?.stateUpdateHandler = { state in
switch state {
case .ready:
print("Listener ready on port \(self.port)")
case .failed(let error):
print("Listener failed with error: \(error)")
self.stop()
case .cancelled:
print("Listener cancelled.")
default:
break
}
}
listener?.newConnectionHandler = { newConnection in
print("New connection established from \(String(describing: newConnection.endpoint))")
self.handleConnection(newConnection)
}
listener?.start(queue: .main)
}
func stop() {
listener?.cancel()
listener = nil
print("Listener stopped.")
}
private func handleConnection(_ connection: NWConnection) {
connection.stateUpdateHandler = { state in
switch state {
case .ready:
print("Connection ready: \(connection.debugDescription)")
self.receive(on: connection)
case .failed(let error):
print("Connection failed: \(error)")
connection.cancel()
case .cancelled:
print("Connection cancelled: \(connection.debugDescription)")
default:
break
}
}
connection.start(queue: .main)
}
private func receive(on connection: NWConnection) {
connection.receiveMessage { (content, context, isComplete, error) in
if let content = content, !content.isEmpty {
let receivedString = String(data: content, encoding: .utf8) ?? "Undecodable data"
print("Received from \(connection.endpoint): \(receivedString)")
// Echo back the received data
connection.send(content: content, completion: .contentProcessed({ sendError in
if let sendError = sendError {
print("Send error: \(sendError)")
}
}))
}
if let error = error {
print("Receive error: \(error)")
connection.cancel()
} else if isComplete {
print("Connection complete from \(connection.endpoint)")
connection.cancel()
} else {
self.receive(on: connection) // Continue receiving
}
}
}
}
// --- Usage Example ---
// For demonstration, you can test this by connecting to localhost:8080
// using `netcat` or a web browser after running the Swift script.
// Example netcat command: echo "Hello from netcat" | nc localhost 8080
let echoListener = TCPEchoListener(port: 8080)
do {
try echoListener.start()
print("TCP Echo Listener started on port 8080. Press Enter to stop.")
_ = readLine() // Keep the program running until Enter is pressed
echoListener.stop()
} catch {
print("Failed to start listener: \(error)")
}NWListener accepts incoming connections; the newConnectionHandler closure fires for each new client, and each connection has its own state machine and receive loop. The recursive receive(on:) pattern is the standard way to keep a connection alive for streaming data; the connection stays open until it’s cancelled or the peer closes it. Test with netcat localhost 8080 after running the script.
For something fancier (full TLS, HTTP/2, WebSocket), use SwiftNIO (github.com/apple/swift-nio), which is what Vapor and the rest of the server-side Swift ecosystem are built on. SwiftNIO is event-loop-based, lower-level than the Network framework, and is what you’d reach for if you needed to write a Swift server that competes with nginx for throughput.
OSINT via HTTP APIs#
Most OSINT work involves hitting an HTTP API (Shodan, VirusTotal, HIBP, AbuseIPDB, GitHub, LinkedIn-by-way-of-Google) and parsing the JSON response. Swift’s URLSession and JSONDecoder cover this cleanly:
import Foundation
/// Fetches data from a given URL and attempts to parse it as JSON.
/// - Parameters:
/// - urlString: The URL string of the API endpoint.
/// - completion: A closure that is called with the result: a Dictionary if JSON parsing is successful, or an Error.
func fetchDataFromAPI(urlString: String, completion: @escaping (Result<[String: Any], Error>) -> Void) {
guard let url = URL(string: urlString) else {
completion(.failure(APIError.invalidURL))
return
}
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
completion(.failure(APIError.invalidResponse(statusCode: (response as? HTTPURLResponse)?.statusCode ?? -1)))
return
}
guard let data = data else {
completion(.failure(APIError.noData))
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
completion(.success(json))
} else {
completion(.failure(APIError.jsonParsingFailed))
}
} catch {
completion(.failure(error))
}
}
task.resume()
}
enum APIError: Error, LocalizedError {
case invalidURL
case invalidResponse(statusCode: Int)
case noData
case jsonParsingFailed
var errorDescription: String? {
switch self {
case .invalidURL:
return "The provided URL is invalid."
case .invalidResponse(let statusCode):
return "Invalid HTTP response with status code: \(statusCode)."
case .noData:
return "No data received from the API."
case .jsonParsingFailed:
return "Failed to parse JSON response."
}
}
}
// --- Usage Example ---
// Using a public dummy API for demonstration
let dummyAPIURL = "https://jsonplaceholder.typicode.com/todos/1"
fetchDataFromAPI(urlString: dummyAPIURL) { result in
switch result {
case .success(let json):
print("API Response (JSON):")
for (key, value) in json {
print("- \(key): \(value)")
}
case .failure(let error):
print("Error fetching data: \(error.localizedDescription)")
}
}
// Example with a bad URL
let badAPIURL = "https://this.is.not.a.real.api.com/data"
fetchDataFromAPI(urlString: badAPIURL) { result in
switch result {
case .success(let json):
print("API Response (JSON):")
for (key, value) in json {
print("- \(key): \(value)")
}
case .failure(let error):
print("Error fetching data from bad URL: \(error.localizedDescription)")
}
}The example uses JSONSerialization for type-erased parsing, which is fine for one-off calls. For real OSINT tooling, define Codable structs that match the API’s response schema and decode with JSONDecoder; the compiler then enforces the response shape and you avoid the runtime errors that come from string-keyed dictionary access. Most operator-side API client code in modern Swift looks like this:
struct ShodanHost: Codable {
let ip_str: String
let ports: [Int]
let hostnames: [String]
}
let (data, _) = try await URLSession.shared.data(from: shodanURL)
let host = try JSONDecoder().decode(ShodanHost.self, from: data)The async/await variant of URLSession.data(from:) is what you want in modern Swift; the older completion-handler API still works but produces nested-closure code that’s harder to read.
Payload encoding#
On engagement, payloads frequently need to be encoded for transport (Base64 over HTTP, hex over text protocols, percent-encoded for URLs). Swift’s Data type plus String conversions handle the common cases without a third-party library.
import Foundation
/// Encodes a string to Base64.
/// - Parameter input: The string to encode.
/// - Returns: The Base64 encoded string, or nil if encoding fails.
func base64Encode(input: String) -> String? {
guard let data = input.data(using: .utf8) else { return nil }
return data.base64EncodedString()
}
/// Decodes a Base64 string.
/// - Parameter input: The Base64 string to decode.
/// - Returns: The decoded string, or nil if decoding fails.
func base64Decode(input: String) -> String? {
guard let data = Data(base64Encoded: input) else { return nil }
return String(data: data, encoding: .utf8)
}
/// Converts a string to its hexadecimal representation.
/// - Parameter input: The string to convert.
/// - Returns: The hexadecimal string.
func hexEncode(input: String) -> String {
return input.data(using: .utf8)?.map { String(format: "%02x", $0) }.joined() ?? ""
}
/// Converts a hexadecimal string back to a regular string.
/// - Parameter input: The hexadecimal string.
/// - Returns: The decoded string, or nil if decoding fails.
func hexDecode(input: String) -> String? {
var data = Data(capacity: input.count / 2)
var index = input.startIndex
while index < input.endIndex {
let nextIndex = input.index(index, offsetBy: 2)
if let byte = UInt8(input[index..<nextIndex], radix: 16) {
data.append(byte)
} else {
return nil // Invalid hex character
}
index = nextIndex
}
return String(data: data, encoding: .utf8)
}
// --- Usage Example ---
let originalPayload = "shellcode_payload_here"
// Base64 Encoding/Decoding
if let encodedBase64 = base64Encode(input: originalPayload) {
print("Original: \(originalPayload)")
print("Base64 Encoded: \(encodedBase64)")
if let decodedBase64 = base64Decode(input: encodedBase64) {
print("Base64 Decoded: \(decodedBase64)")
}
}
print("---")
// Hex Encoding/Decoding
let encodedHex = hexEncode(input: originalPayload)
print("Original: \(originalPayload)")
print("Hex Encoded: \(encodedHex)")
if let decodedHex = hexDecode(input: encodedHex) {
print("Hex Decoded: \(decodedHex)")
}
// Example of URL Encoding/Decoding (built-in to Foundation)
let originalURLComponent = "param with spaces & special chars"
if let urlEncoded = originalURLComponent.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
print("\nURL Encoded: \(urlEncoded)")
if let urlDecoded = urlEncoded.removingPercentEncoding {
print("URL Decoded: \(urlDecoded)")
}
}Base64 is built into Data (base64EncodedString() and the Data(base64Encoded:) initializer); URL percent-encoding is built into String (addingPercentEncoding(withAllowedCharacters:) and removingPercentEncoding). Hex encoding requires a custom helper because Data doesn’t ship one, but it’s a two-line function as shown above. For more exotic encodings (UUencode, ROT13, custom obfuscation tied to a string-decryption key compiled into the implant), write the helpers yourself; the standard library doesn’t provide them.
Swift compared to other operator languages#
How Swift stacks up against the languages an operator actually uses for offensive work, organized by what matters per the language’s role:
| Aspect | Swift | Python | Go | Rust |
|---|---|---|---|---|
| Apple-platform integration | Native; first-class | Decent (PyObjC bridge) | Limited | Limited |
| Cross-platform parity | Strong on Linux/Windows, polished on Apple | Excellent everywhere | Excellent everywhere | Excellent everywhere |
| Performance | Compiled, comparable to C++ | Interpreted, ~50x slower | Compiled, ~2-3x slower than Rust | Compiled, fastest of the four |
| Memory safety | ARC + value types; no manual mgmt | GC; no manual mgmt | GC; no manual mgmt | Borrow checker; manual but checked |
| Binary size | Large (Foundation pulls in megabytes) | N/A (interpreted) | Medium (5-15 MB typical) | Small (200KB-2MB typical) |
| Static linking | Hard on Linux; easier on Apple | N/A | Trivial | Trivial |
| Community in offensive work | Small but growing (Mythic Hermes, SwiftBelt) | Largest | Large (Sliver, Mythic agents) | Growing (offensive Rust posts) |
| Typical operator use | macOS/iOS target-side tooling | Operator-host scripting | Cross-platform implants | Loaders, droppers, performance work |
| Learning curve from C | Moderate (new idioms but C-family syntax) | Trivial | Trivial | Steep (borrow checker) |
The practical takeaway: Swift’s offensive use is concentrated on Apple platforms, because that’s where its strengths (CryptoKit, Network framework, native binary distribution, Mach-O quirks the operator can exploit) actually matter. For Linux-target implant work, Go or Rust are better choices. For operator-host glue and one-off scripts, Python is still the right answer.
Real Swift offensive projects worth knowing:
- Mythic Hermes : A Swift 5 macOS agent for the Mythic C2 framework, with in-memory JXA execution support.
- SwiftBelt and Swift-Attack : Cedric Owens’s situational-awareness and post-exploitation harnesses for macOS, both written in Swift specifically to look like normal Apple binaries.
What this comes down to#
Swift is the right language for offensive work that has to land on Apple platforms specifically, and the wrong language for almost everything else. It has more safety than C, more performance than Python, and Apple-native binary characteristics that nothing else gets. It also pulls in Apple’s Foundation framework whether you want it or not, ships larger binaries than Rust or Go, and has a smaller offensive-tools community than any of the alternatives.
If you’re going to do macOS or iOS engagement work, learn Swift. If you’re not, you can get away without it, but read enough of it to recognize what you’re decompiling when you find a Swift binary on a Mac that’s been on someone’s red team engagement. The decompiler will tell you it’s Swift; the demangled symbols and the heap-allocation pattern will give it away even if it didn’t.