├── .gitignore ├── Docs ├── sample_app.png ├── strings_dump.jpg └── strings_plist.png ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── CloakedStringMacro │ └── CloakedStringMacro.swift ├── CloakedStringMacroClient │ └── main.swift └── CloakedStringMacroMacros │ └── CloakedStringMacroMacro.swift └── Tests └── CloakedStringMacroTests └── CloakedStringMacroTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | .swiftpm 34 | 35 | ## Playgrounds 36 | timeline.xctimeline 37 | playground.xcworkspace 38 | 39 | # Swift Package Manager 40 | # 41 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 42 | # Packages/ 43 | # Package.pins 44 | # Package.resolved 45 | # *.xcodeproj 46 | # 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | # .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 94 | -------------------------------------------------------------------------------- /Docs/sample_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pykaso/swiftmacro-cloaked-string/d51e622ca4e9f83cee9f28a9b8fba21036940353/Docs/sample_app.png -------------------------------------------------------------------------------- /Docs/strings_dump.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pykaso/swiftmacro-cloaked-string/d51e622ca4e9f83cee9f28a9b8fba21036940353/Docs/strings_dump.jpg -------------------------------------------------------------------------------- /Docs/strings_plist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pykaso/swiftmacro-cloaked-string/d51e622ca4e9f83cee9f28a9b8fba21036940353/Docs/strings_plist.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lukas Gergel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "4c5b079810523b7c004d703cbb0b10826cf2abb2cdca1702f413da396687c467", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-syntax", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/apple/swift-syntax.git", 8 | "state" : { 9 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", 10 | "version" : "509.1.1" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let package = Package( 8 | name: "CloakedStringMacro", 9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], 10 | products: [ 11 | // Products define the executables and libraries a package produces, making them visible to other packages. 12 | .library( 13 | name: "CloakedStringMacro", 14 | targets: ["CloakedStringMacro"] 15 | ), 16 | .executable( 17 | name: "CloakedStringMacroClient", 18 | targets: ["CloakedStringMacroClient"] 19 | ), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package, defining a module or a test suite. 26 | // Targets can depend on other targets in this package and products from dependencies. 27 | // Macro implementation that performs the source transformation of a macro. 28 | .macro( 29 | name: "CloakedStringMacroMacros", 30 | dependencies: [ 31 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 32 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 33 | ] 34 | ), 35 | 36 | // Library that exposes a macro as part of its API, which is used in client programs. 37 | .target(name: "CloakedStringMacro", dependencies: ["CloakedStringMacroMacros"]), 38 | 39 | // A client of the library, which is able to use the macro in its own code. 40 | .executableTarget(name: "CloakedStringMacroClient", dependencies: ["CloakedStringMacro"]), 41 | 42 | // A test target used to develop the macro implementation. 43 | .testTarget( 44 | name: "CloakedStringMacroTests", 45 | dependencies: [ 46 | "CloakedStringMacroMacros", 47 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 48 | ] 49 | ), 50 | ] 51 | ) 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## CloakedString 2 | 3 | This project presents an extension to the Swift `#cloaked` macro, which is designed to improve the security of open text strings in Swift applications. By converting strings to `Data` format using the `UInt8` field, it makes it much harder to easily extract strings from a decompressed application. This method is particularly useful for protecting sensitive data and strings in Swift projects from reverse engineering efforts. 4 | 5 | The figure below shows a sample application that uses different methods to store sensitive strings. Plain String variable, value stored in Plist and string masked by `#cloaked` macro. 6 | 7 | ![Plist](Docs/sample_app.png) 8 | 9 | Storing sensitive API keys in iOS applications requires careful consideration of security aspects to prevent unauthorized access. The simplest targets for attackers are often keys stored directly in PLIST files, as these files can be easily explored and their information extracted. 10 | 11 | ![Plist](Docs/strings_plist.png) 12 | 13 | 14 | Similarly, during the decompilation of an application, it's relatively easy to identify and read strings containing sensitive data. 15 | 16 | ![Plist](Docs/strings_dump.jpg) 17 | 18 | The most effective strategy for protecting this information, which prevents direct reading of these details, is the dynamic loading of keys after user authentication. This approach significantly complicates the acquisition of sensitive data, as keys are not stored directly in the application and are loaded only when they are truly needed. 19 | 20 | If dynamic loading is not feasible for some reason, an alternative solution can be to store the keys not as plain strings but as byte arrays. This method makes the identification and extraction of keys from the decompiled code much more difficult, as a byte array is not as straightforward to recognize and interpret as text strings. Although no method offers an absolute guarantee of security, employing these practices significantly enhances the protection of sensitive data stored in iOS applications. 21 | 22 | 23 | If you're considering storing api keys or other sensitive strings securely in your iOS app, consider using the `#cloaked` macro. The plain strings used in a compiled app are the easiest thing to read from the app. 24 | 25 | 26 | 27 | ## Features 28 | 29 | - **String masking:** Automatically converts plain text strings to `Data([UInt8])` format, hiding their actual content from analysis. 30 | - **Easy integration:** The extension can be seamlessly integrated into existing projects without requiring extensive code modifications. 31 | - **Enhanced security:** Helps protect sensitive data in the application from unintentional or intentional inspection. 32 | 33 | 34 | ## Installation 35 | 36 | To add the `#cloaked` extension to your Swift project, simply include the `Cloaked.swift` file and follow the instructions to use it. 37 | 38 | 39 | ## Usage 40 | 41 | To use `#cloaked` with your strings, simply wrap your plain text strings with the `#cloaked` macro as shown below: 42 | 43 | Source code: 44 | ```swift 45 | let secureString = #cloaked("secret") 46 | ``` 47 | 48 | Expanded source: 49 | ```swift 50 | String(data: Data([115, 101, 99, 114, 101, 116]), encoding: .utf8)! 51 | ``` 52 | 53 | ## Installation 54 | 55 | ### [Swift Package Manager](https://www.swift.org/package-manager/) (SPM) 56 | 57 | Add the following line to the dependencies in `Package.swift`, to use CloakedStringMacro macro in a SPM project: 58 | 59 | ```swift 60 | .package(url: "https://github.com/pykaso/swiftmacro-cloaked-string.git", from: "0.1.0"), 61 | ``` 62 | 63 | In your target: 64 | 65 | ```swift 66 | .target(name: "", dependencies: [ 67 | .product(name: "CloakedStringMacro", package: "swiftmacro-cloaked-string"), 68 | // ... 69 | ]), 70 | ``` 71 | 72 | Add `import CloakedStringMacro` into your source code to use CloakedStringMacro macro. 73 | 74 | ## Licensing 75 | 76 | This project is available under the MIT License. See the LICENSE file for more information. 77 | 78 | 79 | ## Contributing 80 | 81 | Contributions are warmly welcomed! If you have an idea on how to improve #cloaked, please feel free to create an issue or pull request. 82 | 83 | 84 | -------------------------------------------------------------------------------- /Sources/CloakedStringMacro/CloakedStringMacro.swift: -------------------------------------------------------------------------------- 1 | /// A macro that creates a string initialization from the raw bytes of the source string literal. 2 | /// This converted string is then not easy to find in the decompiled application. 3 | /// For example, 4 | /// 5 | /// #cloaked("sectet") 6 | /// 7 | /// produces: 8 | /// 9 | /// String(data: Data([115, 101, 99, 114, 101, 116]), encoding: .utf8)! 10 | @freestanding(expression) 11 | public macro cloaked(_ value: T) -> (T) = #externalMacro(module: "CloakedStringMacroMacros", type: "CloakedMacro") 12 | -------------------------------------------------------------------------------- /Sources/CloakedStringMacroClient/main.swift: -------------------------------------------------------------------------------- 1 | import CloakedStringMacro 2 | import Foundation 3 | 4 | let safeApiKey = #cloaked("secret") 5 | print(safeApiKey) 6 | 7 | let s = String(data: Data([116, 111, 112, 45, 115, 101, 99, 114, 101, 116, 45, 97, 112, 105, 45, 107, 101, 121]), encoding: .utf8) 8 | print(safeApiKey) 9 | -------------------------------------------------------------------------------- /Sources/CloakedStringMacroMacros/CloakedStringMacroMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | /// Implementation of the `stringify` macro, which takes an expression 7 | /// of any type and produces a tuple containing the value of that expression 8 | /// and the source code that produced the value. For example 9 | /// 10 | /// #stringify(x + y) 11 | /// 12 | /// will expand to 13 | /// 14 | /// (x + y, "x + y") 15 | public struct StringifyMacro: ExpressionMacro { 16 | public static func expansion( 17 | of node: some FreestandingMacroExpansionSyntax, 18 | in context: some MacroExpansionContext 19 | ) -> ExprSyntax { 20 | guard let argument = node.argumentList.first?.expression else { 21 | fatalError("compiler bug: the macro does not have any arguments") 22 | } 23 | 24 | return "(\(argument), \(literal: argument.description))" 25 | } 26 | } 27 | 28 | /// Implementation of the `cloaked` macro, which takes an string 29 | /// and produces string init using data which cannot be easily found in decompiled app 30 | /// 31 | /// #cloaked("sensitive-data") 32 | /// 33 | /// will expand to 34 | /// 35 | /// String(data: Data(..)) 36 | public struct CloakedMacro: ExpressionMacro { 37 | public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax { 38 | guard 39 | /// 1. Grab the first (and only) Macro argument. 40 | let argument = node.argumentList.first?.expression, 41 | /// 2. Ensure the argument contains of a single String literal segment. 42 | let segments = argument.as(StringLiteralExprSyntax.self)?.segments, segments.count == 1, 43 | /// 3. Grab the actual String literal segment. 44 | case let .stringSegment(literalSegment)? = segments.first 45 | else { 46 | throw CloakedMacroError.requiresStaticStringLiteral 47 | } 48 | 49 | let rawString = literalSegment.content.text 50 | guard let data = rawString.data(using: .utf8) else { 51 | throw CloakedMacroError.failedToGetBytesFromString 52 | } 53 | 54 | let bytes = [UInt8](data) 55 | let strBytes = "\(bytes)" 56 | 57 | guard validate(bytes: bytes, originalString: rawString) else { 58 | throw CloakedMacroError.failedToValidateString 59 | } 60 | 61 | return "String(data: Data(\(raw: strBytes)), encoding: .utf8)!" 62 | } 63 | 64 | private static func validate(bytes: [UInt8], originalString: String) -> Bool { 65 | if let reconstructedString = String(bytes: bytes, encoding: .utf8), reconstructedString == originalString { 66 | return true 67 | } 68 | return false 69 | } 70 | } 71 | 72 | enum CloakedMacroError: Error, CustomStringConvertible { 73 | case requiresStaticStringLiteral 74 | case failedToGetBytesFromString 75 | case failedToValidateString 76 | 77 | var description: String { 78 | switch self { 79 | case .requiresStaticStringLiteral: 80 | return "#cloaked requires a static string literal" 81 | case .failedToGetBytesFromString: 82 | return "Conversion of string to bytes failed" 83 | case .failedToValidateString: 84 | return "String back-validation failed" 85 | } 86 | } 87 | } 88 | 89 | @main 90 | struct CloakedStringMacroPlugin: CompilerPlugin { 91 | let providingMacros: [Macro.Type] = [ 92 | StringifyMacro.self, 93 | CloakedMacro.self 94 | ] 95 | } 96 | -------------------------------------------------------------------------------- /Tests/CloakedStringMacroTests/CloakedStringMacroTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxMacrosTestSupport 5 | import XCTest 6 | 7 | #if canImport(CloakedStringMacroMacros) 8 | import CloakedStringMacroMacros 9 | 10 | let testMacros: [String: Macro.Type] = [ 11 | "cloaked": CloakedMacro.self, 12 | ] 13 | #endif 14 | 15 | final class CloakedStringMacroTests: XCTestCase { 16 | func testMacro() throws { 17 | #if canImport(CloakedStringMacroMacros) 18 | assertMacroExpansion( 19 | """ 20 | #cloaked("secret") 21 | """, 22 | expandedSource: """ 23 | String(data: Data([115, 101, 99, 114, 101, 116]), encoding: .utf8)! 24 | """, 25 | macros: testMacros 26 | ) 27 | #else 28 | throw XCTSkip("macros are only supported when running tests for the host platform") 29 | #endif 30 | } 31 | 32 | func testMacroShouldFail() throws { 33 | #if canImport(CloakedStringMacroMacros) 34 | assertMacroExpansion( 35 | #""" 36 | #cloaked(1) 37 | """#, 38 | expandedSource: "#cloaked(1)", 39 | diagnostics: [ 40 | .init(message: "#cloaked requires a static string literal", line: 1, column: 1) 41 | ], 42 | macros: testMacros 43 | ) 44 | #else 45 | throw XCTSkip("macros are only supported when running tests for the host platform") 46 | #endif 47 | } 48 | } 49 | --------------------------------------------------------------------------------