Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@ build/
example/pubspec.lock

# FVM Version Cache
.fvm/
.fvm/

# SPM
.build/
.swiftpm/
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [8.0.1] - 2026-06-26

### Added
- Swift Package Manager (SPM) support for iOS.

## [8.0.0] - 2026-05-13

- Android SDK version: 18.3.0
Expand Down
2 changes: 1 addition & 1 deletion ios/freerasp.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ FreeRASP for iOS is a lightweight and easy-to-use mobile app protection and secu
s.source = { :path => '.' }
s.source_files = 'Classes/**/*', 'TalsecRuntime.xcframework'
s.dependency 'Flutter'
s.platform = :ios, '8.0'
s.platform = :ios, '12.0'

s.preserve_paths = 'TalsecRuntime.xcframework'
s.xcconfig = { 'OTHER_LDFLAGS' => '-framework TalsecRuntime' }
Expand Down
24 changes: 24 additions & 0 deletions ios/freerasp/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
name: "freerasp",
platforms: [
.iOS("12.0")
],
products: [
.library(name: "freerasp", targets: ["freerasp"])
],
dependencies: [],
targets: [
.binaryTarget(
name: "TalsecRuntime",
path: "TalsecRuntime.xcframework"
),
.target(
name: "freerasp",
dependencies: ["TalsecRuntime"],
path: "Sources/freerasp"
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

class ExecutionStateDispatcher {
static let shared = ExecutionStateDispatcher()
private var cache: Set<RaspExecutionStates> = []
private let lock = NSLock()

var listener: ((RaspExecutionStates) -> Void)? {
didSet {
if listener != nil {
flushCache()
}
}
}

func dispatch(event: RaspExecutionStates) {
lock.lock()
defer { lock.unlock() }

if let listener = listener {
listener(event)
} else {
cache.insert(event)
}
}

private func flushCache() {
lock.lock()
let events = cache
cache.removeAll()
lock.unlock()

events.forEach { listener?($0) }
}
}
36 changes: 36 additions & 0 deletions ios/freerasp/Sources/freerasp/Dispatchers/ThreatDispatcher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation
import TalsecRuntime

class ThreatDispatcher {
static let shared = ThreatDispatcher()
private var threatCache: Set<SecurityThreat> = []
private let lock = NSLock()

var listener: ((SecurityThreat) -> Void)? {
didSet {
if listener != nil {
flushCache()
}
}
}

func dispatch(threat: SecurityThreat) {
lock.lock()
defer { lock.unlock() }

if let listener = listener {
listener(threat)
} else {
threatCache.insert(threat)
}
}

private func flushCache() {
lock.lock()
let threats = threatCache
threatCache.removeAll()
lock.unlock()

threats.forEach { listener?($0) }
}
}
17 changes: 17 additions & 0 deletions ios/freerasp/Sources/freerasp/FlutterTalsecConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import TalsecRuntime

/// Model classes which represents data received from Flutter
struct FlutterTalsecConfig : Decodable {
let watcherMail: String
let iosConfig: IOSConfig
let isProd: Bool

func toNativeConfig() -> TalsecConfig {
return TalsecConfig(appBundleIds: iosConfig.bundleIds, appTeamId: iosConfig.teamId, watcherMailAddress: watcherMail, isProd: isProd)
}
}

struct IOSConfig : Decodable {
let bundleIds: [String]
let teamId: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
enum RaspExecutionStates: Int {
case allChecksFinished = 187429
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Flutter

class ExecutionStreamHandler: NSObject, FlutterStreamHandler {
static let shared = ExecutionStreamHandler()

func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
ExecutionStateDispatcher.shared.listener = { state in
events(state.rawValue)
}
return nil
}

func onCancel(withArguments arguments: Any?) -> FlutterError? {
ExecutionStateDispatcher.shared.listener = nil
return nil
}
}
169 changes: 169 additions & 0 deletions ios/freerasp/Sources/freerasp/SwiftFreeraspPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import Flutter
import UIKit
import TalsecRuntime

/// A Flutter plugin that interacts with the Talsec runtime library, handles method calls and provides event streams.
public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {

/// The singleton instance of `SwiftTalsecPlugin`.
static let instance = SwiftFreeraspPlugin()

private override init() {}

/// Registers this plugin with the given `FlutterPluginRegistrar`.
public static func register(with registrar: FlutterPluginRegistrar) {
let messenger = registrar.messenger()
let eventChannel = FlutterEventChannel(name: "talsec.app/freerasp/events", binaryMessenger: messenger)
eventChannel.setStreamHandler(instance)

let executionStateChannel = FlutterEventChannel(name: "talsec.app/freerasp/execution_state", binaryMessenger: messenger)
executionStateChannel.setStreamHandler(ExecutionStreamHandler.shared)

//Channels init
let methodChannel : FlutterMethodChannel = FlutterMethodChannel(name: "talsec.app/freerasp/methods", binaryMessenger: messenger)
registrar.addMethodCallDelegate(instance, channel: methodChannel)
}

/// Handles a method call from Flutter.
///
/// - Parameters:
/// - call: The `FlutterMethodCall` object representing the method call.
/// - result: The `FlutterResult` object to be returned to the caller.
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let args = call.arguments as? Dictionary<String, Any> ?? [:]

switch call.method {
case "start":
start(configJson: args["config"] as? String, result: result)
return
case "blockScreenCapture":
blockScreenCapture(enable: args["enable"] as? Bool, result: result)
return
case "isScreenCaptureBlocked":
isScreenCaptureBlocked(result: result)
return
case "storeExternalId":
storeExternalId(data: args["data"] as? String, result: result)
return
case "removeExternalId":
removeExternalId(result: result)
return
default:
result(FlutterMethodNotImplemented)
}
}

/// Runs Talsec with given configuration
///
/// - Parameters:
/// - args: The arguments received from Flutter which contains configuration
/// - result: The `FlutterResult` object to be returned to the caller.
private func start(configJson: String?, result: @escaping FlutterResult) {
guard let data = configJson?.data(using: .utf8),
let flutterConfig = try? JSONDecoder().decode(FlutterTalsecConfig.self, from: data)
else {
result(FlutterError(code: "configuration-exception", message: "Unable to decode configuration", details: nil))
return
}

Talsec.start(config: flutterConfig.toNativeConfig())

// Flutter expects *some* result to be returned even if it's void
result(nil)
}

/// Blocks screen capture for the current UIWindow.
///
/// - Parameters:
/// - enable: Whether screen capture should be enabled / disabled.
/// - result: The `FlutterResult` object to be returned to the caller.
private func blockScreenCapture(enable: Bool?, result: @escaping FlutterResult){
guard let enableSafe = enable else {
result(FlutterError(code: "block-screen-capture-failure", message: "Couldn't process data.", details: nil))
return
}

getProtectedWindow { window in
if let window = window {
Talsec.blockScreenCapture(enable: enableSafe, window: window)
result(nil)
} else {
result(FlutterError(code: "block-screen-capture-failure", message: "No windows found to block screen capture", details: nil))
}
}
}

/// Determines whether screen capture is blocked for the current UIWindow.
///
/// - Parameters:
/// - nonce: The nonce to be used in the cryptogram calculation.
/// - result: The `FlutterResult` object to be returned to the caller.
private func isScreenCaptureBlocked(result: @escaping FlutterResult){
getProtectedWindow { window in
if let window = window {
let isBlocked = Talsec.isScreenCaptureBlocked(in: window)
result(isBlocked)
} else {
result(FlutterError(code: "is-screen-capture-blocked-failure", message: "Error while checking if screen capture is blocked", details: nil))
}
}
}

private func getProtectedWindow(completion: @escaping (UIWindow?) -> Void) {
DispatchQueue.main.async {
if #available(iOS 13.0, *) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
if let window = windowScene.windows.first {
completion(window)
} else {
completion(nil)
}
} else {
completion(nil)
}
}
}
}

/// Stores the external ID in user defaults.
///
/// - Parameters:
/// - data: The data to be stored.
/// - result: The `FlutterResult` object to be returned to the caller.
private func storeExternalId(data: String?, result: @escaping FlutterResult){
UserDefaults.standard.set(data, forKey: "app.talsec.externalid")
result(nil)
}

/// Removes the external ID from user defaults.
///
/// - Parameters:
/// - result: The `FlutterResult` object to be returned to the caller.
private func removeExternalId(result: @escaping FlutterResult){
UserDefaults.standard.removeObject(forKey: "app.talsec.externalid")
result(nil)
}

/// Attaches a FlutterEventSink to the ThreatDispatcher and processes any detectedThreats in the queue.
///
/// - Parameters:
/// - arguments: Unused
/// - events: The FlutterEventSink to be attached to the ThreatDispatcher.
/// - Returns: Always returns nil.
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
ThreatDispatcher.shared.listener = { threat in
events(threat.callbackIdentifier)
}
return nil
}

// Detaches the current FlutterEventSink from the ThreatDispatcher.
///
/// - Parameters:
/// - arguments: Unused
/// - Returns: Always returns nil.
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
ThreatDispatcher.shared.listener = nil
return nil
}
}
Loading