diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7b68f13 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches-ignore: + - 'bugfix/**' + - 'feature/**' + - 'topic/**' + workflow_dispatch: + +jobs: + tests: + name: Tests + runs-on: macos-12 + env: + DEVELOPER_DIR: "/Applications/Xcode_13.4.1.app/Contents/Developer" + + steps: + - name: Runner Overview + run: system_profiler SPHardwareDataType SPSoftwareDataType SPDeveloperToolsDataType + + - name: Checkout + uses: actions/checkout@v3 + + - name: Upgrade Bash + run: make bash + + - name: Run Tests + run: make tests + + - name: Upload Code Coverage Report + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: ./.coverage/ + fail_ci_if_error: true + verbose: true \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..172d128 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +### macOS ### + +.DS_Store + +### SwiftPM ### + +/.build +/Packages +/.swiftpm + +### Xcode ### + +/*.xcodeproj +xcuserdata/ + +### Other ### + +# Code coverage reports +/.coverage + +# Release artifacts +/.release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..dfd2098 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +conduct@securevale.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ec77fde --- /dev/null +++ b/LICENSE @@ -0,0 +1,211 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a7f3027 --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.DEFAULT_GOAL := help + +.PHONY: help bash release tests + +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + +bash: ## Upgrade Bash to the latest version + @./Scripts/upgrade_bash.sh + +release: ## Generate release artifacts and tag the release with given version + @test -z "$$(git status --porcelain)" || { echo "Aborting due to uncommitted changes" >&2; exit 1; } + @./Scripts/generate_release_artifacts.sh $(version) + @git tag $(version) + @git push origin $(version) + +tests: ## Run package tests + @./Scripts/run_tests.sh diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..5dd9caf --- /dev/null +++ b/Package.resolved @@ -0,0 +1,59 @@ +{ + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "df9ee6676cd5b3bf5b330ec7568a5644f547201b", + "version" : "1.1.3" + } + }, + { + "identity" : "swift-case-paths", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-case-paths", + "state" : { + "revision" : "b4a872984463070c71e2e97e5c02c73a07d0fe36", + "version" : "0.9.0" + } + }, + { + "identity" : "swift-parsing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-parsing.git", + "state" : { + "revision" : "bc92e84968990b41640214b636667f35b6e5d44c", + "version" : "0.10.0" + } + }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-syntax.git", + "state" : { + "revision" : "0b6c22b97f8e9320bca62e82cdbee601cf37ad3f", + "version" : "0.50600.1" + } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "8816142e27f1127d87c61cee4a0a5db93e9df7c4", + "version" : "0.3.1" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams.git", + "state" : { + "revision" : "01835dc202670b5bb90d07f3eae41867e9ed29f6", + "version" : "5.0.1" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..fe1e044 --- /dev/null +++ b/Package.swift @@ -0,0 +1,67 @@ +// swift-tools-version: 5.6 + +import PackageDescription + +var package = Package( + name: "swift-confidential", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .macCatalyst(.v15), + .watchOS(.v8), + .tvOS(.v15) + ], + products: [ + .library( + name: "ConfidentialKit", + targets: ["ConfidentialKit"] + ) + ], + targets: [ + // Client Library + .target(name: "ConfidentialKit"), + + // Tests + .testTarget( + name: "ConfidentialKitTests", + dependencies: ["ConfidentialKit"] + ) + ], + swiftLanguageVersions: [.v5] +) + +#if os(macOS) +package.dependencies.append(contentsOf: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-syntax.git", exact: "0.50600.1"), + .package(url: "https://github.com/pointfreeco/swift-parsing.git", from: "0.9.0"), + .package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0") +]) +package.targets.append(contentsOf: [ + // Core Module + .target( + name: "ConfidentialCore", + dependencies: [ + "ConfidentialKit", + .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), + .product(name: "Parsing", package: "swift-parsing") + ] + ), + + // CLI Tool + .executableTarget( + name: "confidential", + dependencies: [ + "ConfidentialCore", + "Yams", + .product(name: "ArgumentParser", package: "swift-argument-parser") + ] + ), + + // Tests + .testTarget( + name: "ConfidentialCoreTests", + dependencies: ["ConfidentialCore"] + ) +]) +#endif diff --git a/README.md b/README.md new file mode 100644 index 0000000..bfdbe7d --- /dev/null +++ b/README.md @@ -0,0 +1,353 @@ +# Swift Confidential + +[![Swift](https://img.shields.io/badge/Swift-5.6-red)](https://www.swift.org/download) +[![Platforms](https://img.shields.io/badge/Platforms-iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-red)]() +[![SwiftPM](https://img.shields.io/badge/SwiftPM-compatible-brightgreen)](https://github.com/apple/swift-package-manager) + +A highly configurable and performant tool for obfuscating Swift literals embedded in the application code that you should protect from static code analysis, making the app more resistant to reverse engineering. + +Simply integrate the tool with your Swift package, configure your own obfuscation algorithm along with the list of secret literals, and build the project ๐Ÿš€ + +> **NOTE:** Swift Confidential is still in development and even though a lot of thought was put into API design, apart from new features, some breaking changes might still be introduced. See [Versioning](#versioning) section for more information. + +## Motivation + +Pretty much every single app has at least few literals embedded in code, those include: URLs, various client identifiers (e.g. API keys or API tokens), pinning data (e.g. PEM certificates or SPKI digests), Keychain item identifiers, RASP-related literals (e.g. list of suspicious dylibs or list of suspicious file paths for jailbreak detection), and many other context-specific literals. While the listed examples of code literals might seem innocent, not obfuscating them can be considered as giving a handshake to the potential threat actor. This is especially true in security-critical apps, such as mobile banking apps or 2FA authentication apps. As a responsible software engineer, you should be aware that extracting source code literals from the app package is generally easy enough that even less expirienced malicious users can accomplish this with little effort. + +

+ Mach-O C String Literals + A sneak peek at the __TEXT.__cstring section in a sample Mach-O file reveals a lot of interesting information about the app. +

+ +This tool aims to provide an elegant and maintainable solution to the above problem by introducing the composable obfuscation techniques that can be freely combined to form an algorithm for obfuscating selected Swift literals. + +> **NOTE:** While Swift Confidential certainly makes the static analysis of the code more challenging, **it is by no means the only code hardening technique that you should employ to protect your app against reverse engineering and tampering**. To achieve a decent level of security, we highly encourage you to supplement this tool's security measures with **runtime application self-protection (RASP) checks**, as well as **Swift code obfuscation**. With that said, no security measure can ever guarantee absolute security. Any motivated and skilled enough attacker will eventually bypass all security protections. For this reason, **always keep your threat models up to date**. + +## Getting started + +Begin by creating a `confidential.yml` YAML configuration file in the root directory of your target's sources. At minimum, the configuration must contain obfuscation algorithm and one or more secret definitions. + +For example, a configuration file for the hypothetical `RASP` module could look like this: + +```yaml +algorithm: + - encrypt using aes-192-gcm + - shuffle +defaultNamespace: extend RASP.Literals +secrets: + - name: suspiciousDynamicLibraries + value: + - Substrate + - Substitute + - frida + # ... other suspicious dylibs + - name: suspiciousFilePaths + value: + - /.installed_unc0ver + - /usr/sbin/frida-server + - /private/var/lib/cydia + # ... other suspicious file paths +``` + +> **WARNING:** The algorithm from the above configuration serves as example only, **do not use this particular algorithm in your production code**. Instead, compose your own algorithm from the [obfuscation techniques](#obfuscation-techniques) described below and **don't share your algorithm with anyone**. Moreover, following the [secure SDLC](https://owasp.org/www-project-integration-standards/writeups/owasp_in_sdlc/) best practices, consider not to commit the production algorithm in your repository, but instead configure your CI/CD pipeline to run a custom script (ideally just before the build step), which will modify the configuration file by replacing the algorithm value with the one retrieved from the secrets vault. + +Having created the configuration file, you can use [Confidential Swift Package plugin](https://github.com/securevale/swift-confidential-plugin) (see [this section](#integrating-with-your-swiftpm-project) for details) to generate Swift code with obfuscated secret literals. + +Under the hood, the Confidential plugin invokes the `confidential` CLI tool by issuing the following command: + +```sh +confidential obfuscate --configuration "path/to/confidential.yml" --output "path/to/Confidential.generated.swift" +``` + +Upon successful command execution, the generated `Confidential.generated.swift` file will contain code similar to the following: + +```swift +import ConfidentialKit +import Foundation + +extension RASP.Literals { + + @ConfidentialKit.Obfuscated>(deobfuscateData) + static var suspiciousDynamicLibraries: ConfidentialKit.Obfuscation.Secret = .init(data: [0x4a, 0x84, 0x89, 0x73, 0x7c, 0x81, 0x18, 0x86, 0x16, 0x5e, 0x1c, 0x41, 0xdc, 0x2e, 0xe1, 0xe4, 0x92, 0x98, 0xdc, 0xde, 0x98, 0xa5, 0xa7, 0x31, 0x6a, 0x5f, 0x4c, 0x7e, 0x5d, 0x61, 0xfd, 0x9d, 0xc1, 0xd2, 0xd4, 0xb8, 0xaf, 0xba, 0xa3, 0x46, 0xda, 0x61, 0xcb, 0xf1, 0xb7, 0xbd, 0xf9, 0xc7, 0x3a, 0x6d, 0xe8, 0x62, 0x09, 0x29, 0x6e, 0x13, 0x5b, 0x50, 0xfa, 0xde, 0x80, 0x82, 0x80, 0x7e, 0xe2, 0x3c, 0xf0, 0xf1, 0x03, 0x12, 0xf8, 0x50, 0x95, 0x03, 0xc8, 0x4e, 0xc1, 0xb3, 0xa9, 0x2c, 0xed, 0x0b, 0x1b, 0x71, 0xe8, 0xfd, 0xa2, 0x69, 0xca, 0xac, 0x4f, 0x35, 0xc6, 0x4f, 0x01, 0x36, 0x5a, 0x5d, 0x58, 0x3b, 0x37, 0x0b, 0x0c, 0x4e, 0x24, 0x1f, 0x38, 0x25, 0x33, 0x3d, 0x4c, 0x27, 0x1c, 0x20, 0x15, 0x01, 0x07, 0x26, 0x0a, 0x51, 0x1a, 0x3c, 0x11, 0x18, 0x21, 0x12, 0x1e, 0x29, 0x3f, 0x5f, 0x0e, 0x19, 0x09, 0x57, 0x31, 0x04, 0x32, 0x4f, 0x2f, 0x02, 0x35, 0x06, 0x23, 0x03, 0x08, 0x1d, 0x2c, 0x39, 0x2d, 0x10, 0x5e, 0x48, 0x05, 0x28, 0x0d, 0x52, 0x5c, 0x4d, 0x30, 0x17, 0x2e, 0x5b, 0x34, 0x1b, 0x56, 0x49, 0x22, 0x00, 0x53, 0x55, 0x16, 0x2a, 0x50, 0x13, 0x54, 0x2b, 0x59, 0x3a, 0x3e, 0x0f, 0x14]) + + @ConfidentialKit.Obfuscated>(deobfuscateData) + static var suspiciousFilePaths: ConfidentialKit.Obfuscation.Secret = .init(data: [0x99, 0x84, 0x89, 0x73, 0x7c, 0x81, 0x18, 0x86, 0xe0, 0x5b, 0x88, 0x65, 0xa5, 0x1f, 0x53, 0x5e, 0x3c, 0xa5, 0x58, 0x0b, 0x80, 0x06, 0xad, 0x6d, 0x5e, 0xae, 0x2d, 0x52, 0xea, 0xf1, 0xde, 0xac, 0x9d, 0x36, 0x12, 0x56, 0xf9, 0xce, 0xe4, 0x95, 0x84, 0x7e, 0x47, 0x44, 0xde, 0xd3, 0x76, 0x68, 0x67, 0x90, 0xfb, 0x30, 0x3c, 0xaf, 0x33, 0xce, 0x8e, 0x79, 0xa5, 0xdb, 0x7a, 0x97, 0x9d, 0x60, 0x58, 0x0f, 0x59, 0x5f, 0x2c, 0xe0, 0x3a, 0x4d, 0xf6, 0x38, 0x75, 0x1c, 0x2e, 0x5c, 0x94, 0x3f, 0x7c, 0x7a, 0x61, 0xca, 0xbe, 0xbf, 0x52, 0x05, 0x0d, 0xc7, 0xe3, 0xe2, 0x8e, 0x34, 0x02, 0x5f, 0x6b, 0x56, 0xb0, 0x67, 0xac, 0xf3, 0xf8, 0x3d, 0xf5, 0x26, 0xa0, 0x3b, 0x91, 0xf4, 0x88, 0x38, 0xc4, 0x5b, 0xaa, 0x7a, 0x0c, 0x2e, 0x98, 0xeb, 0xd2, 0xbd, 0x7e, 0x63, 0x53, 0xf7, 0x37, 0xb4, 0xc0, 0xb8, 0x8f, 0xb6, 0xe1, 0xd4, 0x3c, 0x89, 0x7c, 0x0f, 0xa8, 0x2a, 0xea, 0x01, 0x0c, 0x74, 0x7b, 0x65, 0x4c, 0x78, 0x14, 0x0f, 0x09, 0x60, 0x02, 0x2f, 0x1f, 0x4a, 0x2c, 0x1a, 0x52, 0x1e, 0x37, 0x98, 0x71, 0x17, 0x30, 0x34, 0x0a, 0x3c, 0x5f, 0x6a, 0x58, 0x42, 0x13, 0x39, 0x44, 0x4d, 0x79, 0x41, 0x59, 0x4b, 0x7d, 0x64, 0x20, 0x33, 0x21, 0x50, 0x5d, 0x22, 0x45, 0x28, 0x54, 0x4f, 0x66, 0x6d, 0x77, 0x03, 0x11, 0x25, 0x26, 0x08, 0x6c, 0x9d, 0x2b, 0x12, 0x6e, 0x68, 0x5e, 0x69, 0x04, 0x9e, 0x06, 0x0d, 0x3a, 0x3e, 0x63, 0x62, 0x2a, 0x19, 0x55, 0x73, 0x01, 0x24, 0x7a, 0x1c, 0x61, 0x32, 0x23, 0x2e, 0x7c, 0x35, 0x46, 0x1b, 0x5b, 0x76, 0x36, 0x4e, 0x7f, 0x16, 0x3f, 0x51, 0x7e, 0x0b, 0x27, 0x9c, 0x10, 0x57, 0x31, 0x18, 0x3b, 0x2d, 0x1d, 0x47, 0x67, 0x0e, 0x6b, 0x72, 0x6f, 0x56, 0x53, 0x40, 0x9f, 0x5c, 0x05, 0x3d, 0x49, 0x00, 0x5a, 0x15, 0x43, 0x38, 0x29, 0x75, 0x70, 0x48, 0x07]) + + @inline(__always) + private static func deobfuscateData(_ data: Foundation.Data) throws -> Foundation.Data { + try ConfidentialKit.Obfuscation.Encryption.DataCrypter(algorithm: .aes192GCM) + .deobfuscate( + try ConfidentialKit.Obfuscation.Randomization.DataShuffler(nonce: 9662615372037719068) + .deobfuscate(data) + ) + } +} +``` + +You can then, for example, iterate over a deobfuscated array of suspicious dynamic libraries in your own code using the projected value of the generated `suspiciousDynamicLibraries` property: + +```swift +let suspiciousLibraries = RASP.Literals.$suspiciousDynamicLibraries + .map { $0.lowercased() } +let checkPassed = loadedLibraries + .allSatisfy { !suspiciousLibraries.contains(where: $0.lowercased().contains) } +``` + +### Integrating with your SwiftPM project + +To use Swift Confidential with your SwiftPM package, add the `ConfidentialKit` library along with `Confidential` plugin to the package's dependencies and then to your target's dependencies and plugins respectively: + +```swift +// swift-tools-version: 5.6 + +import PackageDescription + +let package = Package( + name: "MyLibrary", + products: [ + .library(name: "MyLibrary", targets: ["MyLibrary"]) + ], + dependencies: [ + // other dependencies + .package(url: "https://github.com/securevale/swift-confidential.git", from: "0.1.0"), + .package(url: "https://github.com/securevale/swift-confidential-plugin.git", from: "0.1.0") + ], + targets: [ + .target( + name: "MyLibrary", + dependencies: [ + // other dependencies + .product(name: "ConfidentialKit", package: "swift-confidential") + ], + exclude: ["confidential.yml"], + plugins: [ + // other plugins + .plugin(name: "Confidential", package: "swift-confidential-plugin") + ] + ) + ] +) +``` + +Please make sure to add a path to the `confidential.yml` configuration file to target's `exclude` list to explicitly exclude this file from the target's resources. + +Now simply build the `MyLibrary` target and the plugin will automatically generate a Swift source file with obfuscated secret literals. In addition, the plugin will regenerate the obfuscated secret literals every time it detects a change to `confidential.yml` configuration file or when you clean build your project. + +> **NOTE:** Swift 5.6 is required in order to run the plugin. +> Also, please make sure to use the same version requirements for both `swift-confidential` and `swift-confidential-plugin` packages. + +## Configuration + +Swift Confidential supports a number of configuration options, all of which are stored in a single YAML configuration file. + +### YAML configuration keys + +The table below lists the keys to include in the configuration file along with the type of information to include in each. Any other keys in the configuration file are ignored by the CLI tool. + +| Key | Value type | Description | +|------------------|--------------------------|-----------------------------------------------------------------------------------| +| algorithm | List of strings | The list of obfuscation techniques representing individual steps that are composed together to form the obfuscation algorithm. See [Obfuscation techniques](#obfuscation-techniques) section for usage details.
**Required.** | +| defaultNamespace | String | The default namespace in which to enclose all the generated secret literals without explicitly assigned namespace. The default value is `extend Obfuscation.Secret from ConfidentialKit`. See [Namespaces](#namespaces) section for usage details. | +| secrets | List of objects | The list of objects defining the secret literals to be obfuscated. See [Secrets](#secrets) section for usage details.
**Required.** | + +
+Example configuration + +```yaml +algorithm: + - encrypt using aes-192-gcm + - shuffle +defaultNamespace: create Secrets +secrets: + - name: apiToken + value: 214C1E2E-A87E-4460-8205-4562FDF54D1C + - name: trustedSPKIDigests + value: + - 7a6820614ee600bbaed493522c221c0d9095f3b4d7839415ffab16cbf61767ad + - cf84a70a41072a42d0f25580b5cb54d6a9de45db824bbb7ba85d541b099fd49f + - c1a5d45809269301993d028313a5c4a5d8b2f56de9725d4d1af9da1ccf186f30 + namespace: extend Pinning from Crypto +``` + +> **WARNING:** The algorithm from the above configuration serves as example only, **do not use this particular algorithm in your production code**. +
+ +### Obfuscation techniques + +The obfuscation techniques are the composable building blocks from which you can create your own obfuscation algorithm. You can compose them in any order you want, so that no one exept you knows how the secret literals are obfuscated. + +#### Compression + +This technique involves data compression using the algorithm of your choice. The compression technique is **non-polymorphic**, meaning that given the same input data, the same output data is produced with each run. + +**Syntax** + +```yaml +compress using +``` + +The supported algorithms are shown in the following table: +| Algorithm | Description | +|------------------|-----------------------------------------------------------| +| lzfse | The LZFSE compression algorithm. | +| lz4 | The LZ4 compression algorithm. | +| lzma | The LZMA compression algorithm. | +| zlib | The zlib compression algorithm. | + +#### Encryption + +This technique involves data encryption using the algorithm of your choice. The encryption technique is **polymorphic**, meaning that given the same input data, different output data is produced with each run. + +**Syntax** + +```yaml +encrypt using +``` + +The supported algorithms are shown in the following table: +| Algorithm | Description | +|------------------|-------------------------------------------------------------------------------------------------| +| aes-128-gcm | The Advanced Encryption Standard (AES) algorithm in Galois/Counter Mode (GCM) with 128-bit key. | +| aes-192-gcm | The Advanced Encryption Standard (AES) algorithm in Galois/Counter Mode (GCM) with 192-bit key. | +| aes-256-gcm | The Advanced Encryption Standard (AES) algorithm in Galois/Counter Mode (GCM) with 256-bit key. | +| chacha20-poly | The ChaCha20-Poly1305 algorithm. | + +#### Randomization + +This technique involves data randomization. The randomization technique is **polymorphic**, meaning that given the same input data, different output data is produced with each run. + +> **NOTE:** Randomization technique is best suited for secrets of which size does not exceed 256 bytes. +> For larger secrets, the size of the obfuscated data will grow from 2N to 3N, where N is the input data size in bytes, +> or even 5N (32-bit platform) or 9N (64-bit platform) if the size of input data is larger than 65 536 bytes. +> For this reason, the internal implementation of this technique is a subject to change in next releases. + +**Syntax** + +```yaml +shuffle +``` + +### Secrets + +The configuration file utilizes YAML objects to describe the secret literals, which are to be obfuscated. The table below lists the keys to define secret literal along with the type of information to include in each. + +| Key | Value type | Description | +|------------------|---------------------------|----------------------------------------------------------------------------------| +| name | String | The name of the generated Swift property containing obfuscated secret literal's data. This value is used as-is, without validity checking. Thus, make sure to use a valid property name.
**Required.** | +| namespace | String | The namespace in which to enclose the generated secret literal declaration. See [Namespaces](#namespaces) section for usage details. | +| value | String or List of strings | The plain value of the secret literal, which is to be obfuscated. The YAML data types are mapped to `String` and `Array` in Swift, respectively.
**Required.** | + +
+Example secret definition + +Supposing that you would like to obfuscate the tag used to reference the private key stored in Keychain or Secure Enclave: + +```yaml +name: secretVaultKeyTag +value: com.example.app.keys.secret_vault_private_key +namespace: extend KeychainAccess.Key from Crypto +``` + +The above YAML secret definition will result in the following Swift code being generated: + +```swift +import Crypto +// ... other imports + +extension Crypto.KeychainAccess.Key { + + @ConfidentialKit.Obfuscated(deobfuscateData) + static var secretVaultKeyTag: ConfidentialKit.Obfuscation.Secret = .init(data: [/* obfuscated data */]) +} +``` + +You may also need to obfuscate a list of related values, such as a list of trusted SPKI digests to pin against: + +```yaml +name: trustedSPKIDigests +value: + - 7a6820614ee600bbaed493522c221c0d9095f3b4d7839415ffab16cbf61767ad + - cf84a70a41072a42d0f25580b5cb54d6a9de45db824bbb7ba85d541b099fd49f + - c1a5d45809269301993d028313a5c4a5d8b2f56de9725d4d1af9da1ccf186f30 +namespace: extend Pinning from Crypto +``` + +With the above YAML secret definition, the following Swift code will be generated: + +```swift +import Crypto +// ... other imports + +extension Crypto.Pinning { + + @ConfidentialKit.Obfuscated>(deobfuscateData) + static var trustedSPKIDigests: ConfidentialKit.Obfuscation.Secret = .init(data: [/* obfuscated data */]) +} +``` +
+ +### Namespaces + +In accordance with Swift programming best practices, Swift Confidential encapsulates generated secret literal declarations in namespaces (typically enums). The namespaces syntax allows you to either create a new namespace or extend an existing one. + +**Syntax** + +```yaml +create # creates new namespace + +extend [from ] # extends existing namespace, optionally specifying + # the module to which this namespace belongs +``` + +
+Example usage + +Assuming that you would like to keep the generated secret literal declaration(s) in a new namespace named `Secrets`, use the following YAML code: + +```yaml +create Secrets +``` + +The above namespace definition will result in the following Swift code being generated: + +```swift +enum Secrets { + + // Encapsulated declarations ... +} + +``` + +> **NOTE:** The creation of the nested namespaces is currently not supported. + +If, however, you would rather like to keep the generated secret literal declaration(s) in an existing namespace named `Pinning` and imported from `Crypto` module, use the following YAML code instead: + +```yaml +extend Pinning from Crypto +``` + +With the above namespace definition, the following Swift code will be generated: + +```swift +import Crypto +// ... other imports + +extension Crypto.Pinning { + + // Encapsulated declarations ... +} +``` +
+ +### Additional considerations for Confidential Swift Package plugin + +The [Confidential Swift Package plugin](https://github.com/securevale/swift-confidential-plugin) expects the configuration file to be named `confidential.yml` or `confidential.yaml`, and it assumes a single configuration file per target. If you define multiple configuration files in different subdirectories, then the plugin will use the first one it finds, and which one is undefined. + +## Versioning + +This project follows [semantic versioning](https://semver.org/). While still in major version `0`, source-stability is only guaranteed within minor versions (e.g. between `0.1.0` and `0.1.1`). If you want to guard against potentially source-breaking package updates, you can specify your package dependency using version range (e.g. `"0.1.0"<.."0.2.0"`) as the requirement. + +## License + +This tool and code is released under Apache License v2.0 with Runtime Library Exception. +Please see [LICENSE](LICENSE) for more information. \ No newline at end of file diff --git a/Resources/machoview-cstring-literals.png b/Resources/machoview-cstring-literals.png new file mode 100644 index 0000000..d48fef6 Binary files /dev/null and b/Resources/machoview-cstring-literals.png differ diff --git a/Scripts/Commons/common.sh b/Scripts/Commons/common.sh new file mode 100644 index 0000000..542ca15 --- /dev/null +++ b/Scripts/Commons/common.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC2034,SC2155 + +##################### +# CONSTANTS # +##################### + +if [[ -t 1 && -t 2 ]] +then + # shellcheck disable=SC2086 + function tput_set() { tput $1; } +else + function tput_set() { :; } +fi + +readonly BOLD=$(tput_set bold) +readonly UNDERLINE=$(tput_set smul) +readonly NORMAL=$(tput_set sgr0) + +##################### +# FUNCTIONS # +##################### + +function pushd_quiet() { + pushd "$1" &>/dev/null || exit +} + +function popd_quiet() { + popd &>/dev/null || exit +} + +function echoerr() { + local IFS=" " + cat <<< "$* โŒ" 1>&2; +} + +function echo_progress() { + echo "$1 โš™๏ธ" +} diff --git a/Scripts/Templates/artifactbundle-info.json.template b/Scripts/Templates/artifactbundle-info.json.template new file mode 100644 index 0000000..f43d382 --- /dev/null +++ b/Scripts/Templates/artifactbundle-info.json.template @@ -0,0 +1,15 @@ +{ + "schemaVersion": "1.0", + "artifacts": { + "__NAME__": { + "type": "executable", + "version": "__VERSION__", + "variants": [ + { + "path": "__NAME__-__VERSION__-macos/bin/__NAME__", + "supportedTriples": ["x86_64-apple-macosx", "arm64-apple-macosx"] + } + ] + } + } +} \ No newline at end of file diff --git a/Scripts/generate_release_artifacts.sh b/Scripts/generate_release_artifacts.sh new file mode 100755 index 0000000..d1e3148 --- /dev/null +++ b/Scripts/generate_release_artifacts.sh @@ -0,0 +1,224 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091,SC2155 + +set -Eeuo pipefail + +source "$(dirname "$0")"/Commons/common.sh + +################# +# CONSTANTS # +################# + +readonly SCRIPT_NAME=$(basename -s ".sh" "$0") +readonly SCRIPT_FULL_NAME=$(basename "$0") +readonly SCRIPT_ABS_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)" +readonly SCRIPT_TEMPLATES_ABS_PATH="$SCRIPT_ABS_PATH/Templates" + +readonly OPTION_HELP_SHORT="-h" +readonly OPTION_HELP="--help" + +readonly PRODUCT_CONFIDENTIAL="confidential" + +readonly SWIFT_BUILD_ARCH_X86="x86_64" +readonly SWIFT_BUILD_ARCH_ARM="arm64" +readonly SWIFT_BUILD_DIR_NAME=".build" + +readonly UNIVERSAL_BIN_DIR_ABS_PATH="$SCRIPT_ABS_PATH/../$SWIFT_BUILD_DIR_NAME/universal" + +readonly LICENSE_ABS_PATH="$SCRIPT_ABS_PATH/../LICENSE" +readonly ARTIFACT_BUNDLE_INFO_TEMPLATE_ABS_PATH="$SCRIPT_TEMPLATES_ABS_PATH/artifactbundle-info.json.template" + +readonly RELEASE_DIR_NAME=".release" +readonly RELEASE_DIR_ABS_PATH="$SCRIPT_ABS_PATH/../$RELEASE_DIR_NAME" + +readonly ERROR_MSG_SEE_USAGE_HELP="Use ${BOLD}$OPTION_HELP_SHORT${NORMAL} | ${BOLD}$OPTION_HELP${NORMAL} option for usage help." + +######################## +# GLOBAL VARIABLES # +######################## + +VERSION_STRING="" + +TMP_DIR_PATH="" +UNIVERSAL_BIN_ABS_PATH="" + +################# +# FUNCTIONS # +################# + +function help() { + cat << MANUAL + +${BOLD}NAME${NORMAL} + ${BOLD}$SCRIPT_NAME${NORMAL} + +${BOLD}SYNOPSIS${NORMAL} + ${BOLD}$SCRIPT_FULL_NAME${NORMAL} ${UNDERLINE}version${NORMAL} + ${BOLD}$SCRIPT_FULL_NAME${NORMAL} ${BOLD}$OPTION_HELP_SHORT${NORMAL} | ${BOLD}$OPTION_HELP${NORMAL} + +${BOLD}DESCRIPTION${NORMAL} + ${BOLD}$SCRIPT_NAME${NORMAL} is a script that generates the release artifacts tagging + them with the supplied ${UNDERLINE}version${NORMAL} string. The generated artifacts are saved in the + ${BOLD}$RELEASE_DIR_NAME${NORMAL} directory located in the package's root directory. + + Generated artifacts include: + โ€ข The zip archive containing SwiftPM artifact bundle with ${BOLD}$PRODUCT_CONFIDENTIAL${NORMAL} CLI tool + binary for macOS. + +${BOLD}DEPENDENCIES${NORMAL} + The ${BOLD}$SCRIPT_NAME${NORMAL} script has the following dependencies: + โ€ข ${BOLD}Bash 4.2 or newer${NORMAL} - you can upgrade Bash with ${UNDERLINE}upgrade_bash.sh${NORMAL} script. + โ€ข ${BOLD}Swift 5.6${NORMAL} - Swift toolchain comes bundled with Xcode. + + Make sure that all dependencies are installed before you start using the script. + +${BOLD}ARGUMENTS${NORMAL} + + ${UNDERLINE}version${NORMAL} + The release version following the MAJOR.MINOR.PATCH scheme. + +${BOLD}OPTIONS${NORMAL} + Options start with one or two dashes. + + ${BOLD}$OPTION_HELP_SHORT${NORMAL}, ${BOLD}$OPTION_HELP${NORMAL} + Show usage description. + +${BOLD}EXAMPLE USAGE${NORMAL} + Generate artifacts for release tagged with version 0.0.1. + ${BOLD}$SCRIPT_FULL_NAME 0.0.1${NORMAL} + +MANUAL +} + +function read_version_argument() { + if [[ $# -eq 0 ]] + then + echoerr "No version provided. $ERROR_MSG_SEE_USAGE_HELP" + exit 1 + fi + if [[ ! $1 =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] + then + echoerr "Invalid version format: $1" + exit 1 + fi + + VERSION_STRING="$1" +} + +function set_up() { + echo "---------------------------- SET UP ----------------------------" + echo "๐Ÿณ" + + echo "Cleaning up SPM build artifacts" + swift package clean + + echo "Making temporary output directory" + TMP_DIR_PATH=$(mktemp -d "$(pwd -P)/$SCRIPT_NAME.tmp.XXXXXXXXXX") + + echo "-------------------------- END SET UP --------------------------" +} + +function clean_up() { + echo "--------------------------- CLEAN UP ---------------------------" + echo "๐Ÿงฝ" + + # SPM .build directory generated by SPM interferes with + # Xcode build system, so it needs to be removed once script execution is done. + local -r swift_build_dir_path="$SCRIPT_ABS_PATH/../$SWIFT_BUILD_DIR_NAME" + if [[ -d "$swift_build_dir_path" ]] + then + echo "Deleting SPM $SWIFT_BUILD_DIR_NAME directory" + rm -rf "$swift_build_dir_path" + fi + + if [[ -d "$TMP_DIR_PATH" ]] + then + echo "Removing temporary output directory" + rm -rf "$TMP_DIR_PATH" + fi + + echo "------------------------- END CLEAN UP -------------------------" +} + +function swift_build_cmd() { + echo "swift build --product $1 --configuration release -Xlinker -dead_strip --arch $2" +} + +function build_product() { + eval "$(swift_build_cmd "$1" "$2") > /dev/null" + local -r bin_path=$(eval "$(swift_build_cmd "$1" "$2") --show-bin-path") + + echo "$bin_path/$1" +} + +function build_universal_binary() { + local -r product="$1" + echo_progress "Building $product product for $SWIFT_BUILD_ARCH_X86 architecture" + local -r x86_bin_path=$(build_product "$product" "$SWIFT_BUILD_ARCH_X86") + echo_progress "Building $product product for $SWIFT_BUILD_ARCH_ARM architecture" + local -r arm_bin_path=$(build_product "$product" "$SWIFT_BUILD_ARCH_ARM") + + echo_progress "Creating fat binary for $SWIFT_BUILD_ARCH_X86+$SWIFT_BUILD_ARCH_ARM" + mkdir -p "$UNIVERSAL_BIN_DIR_ABS_PATH" + UNIVERSAL_BIN_ABS_PATH="$UNIVERSAL_BIN_DIR_ABS_PATH/$product" + lipo "$x86_bin_path" "$arm_bin_path" -create -output "$UNIVERSAL_BIN_ABS_PATH" + strip -rSTx "$UNIVERSAL_BIN_ABS_PATH" +} + +function spm_artifactbundle() { + echo "---------------------- SPM ARTIFACT BUNDLE ---------------------" + + local -r product="$1" + build_universal_binary "$product" + + echo_progress "Generating SPM artifact bundle" + local -r bundle_name="$product.artifactbundle" + local -r bundle_path="$TMP_DIR_PATH/$bundle_name" + local -r bundle_bin_path="$bundle_path/$product-$VERSION_STRING-macos/bin" + mkdir -p "$bundle_bin_path" + sed "s/__NAME__/$product/g; s/__VERSION__/$VERSION_STRING/g" "$ARTIFACT_BUNDLE_INFO_TEMPLATE_ABS_PATH" > "$bundle_path/info.json" + cp -f "$UNIVERSAL_BIN_ABS_PATH" "$bundle_bin_path" + cp -f "$LICENSE_ABS_PATH" "$bundle_path" + + mkdir -p "$RELEASE_DIR_ABS_PATH" + + echo_progress "Archiving SPM artifact bundle" + local -r bundle_archive_name="${product^}Binary-macos.artifactbundle.zip" + pushd_quiet "$TMP_DIR_PATH" + zip -qr "$RELEASE_DIR_ABS_PATH/$bundle_archive_name" "$bundle_name" + popd_quiet + + pushd_quiet "$SCRIPT_ABS_PATH/.." + echo -e "Bundle checksum:\n$(swift package compute-checksum "./$RELEASE_DIR_NAME/$bundle_archive_name")" + popd_quiet + + echo "-------------------- END SPM ARTIFACT BUNDLE -------------------" +} + +################### +# ENTRY POINT # +################### + +while [[ $# -gt 0 ]] +do +key="$1" + +case $key in + "$OPTION_HELP_SHORT" | "$OPTION_HELP") + help + exit 0 + ;; + *) + break + ;; +esac +done + +read_version_argument "$@" + +trap clean_up EXIT + +set_up + +spm_artifactbundle "$PRODUCT_CONFIDENTIAL" diff --git a/Scripts/run_tests.sh b/Scripts/run_tests.sh new file mode 100755 index 0000000..bce127b --- /dev/null +++ b/Scripts/run_tests.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +# shellcheck disable=SC1091,SC2155 + +set -Eeuo pipefail + +source "$(dirname "$0")"/Commons/common.sh + +################# +# CONSTANTS # +################# + +readonly SCRIPT_ABS_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd -P)" + +readonly SWIFT_BUILD_DIR_NAME=".build" + +readonly COVERAGE_DIR_NAME=".coverage" +readonly COVERAGE_INCLUDE_LIST=( + "Sources/ConfidentialCore/" + "Sources/ConfidentialKit/" + ) + +################# +# FUNCTIONS # +################# + +function set_up() { + echo "---------------------------- SET UP ----------------------------" + echo "๐Ÿณ" + + echo "Cleaning up SPM build artifacts" + swift package clean + + echo "-------------------------- END SET UP --------------------------" +} + +function clean_up() { + echo "--------------------------- CLEAN UP ---------------------------" + echo "๐Ÿงฝ" + + # SPM .build directory generated by SPM interferes with + # Xcode build system, so it needs to be removed once script execution is done. + local -r swift_build_dir_path="$SCRIPT_ABS_PATH/../$SWIFT_BUILD_DIR_NAME" + if [[ -d "$swift_build_dir_path" ]] + then + echo "Deleting SPM $SWIFT_BUILD_DIR_NAME directory" + rm -rf "$swift_build_dir_path" + fi + + echo "------------------------- END CLEAN UP -------------------------" +} + +function swift_test() { + echo "-------------------------- SWIFT TEST --------------------------" + echo "๐Ÿงช" + + swift test --parallel --enable-code-coverage + + echo "------------------------ END SWIFT TEST ------------------------" +} + +function swift_package_name() { + swift package describe | awk '/Name:/ { print $2; exit; }' +} + +function export_coverage_data() { + echo "------------------------ COVERAGE DATA -------------------------" + + echo_progress "Exporting code coverage data" + + local -r package_name=$(swift_package_name) + local -r bin_path="$SWIFT_BUILD_DIR_NAME/debug/${package_name}PackageTests.xctest/Contents/MacOS/${package_name}PackageTests" + local -r profdata_path="$SWIFT_BUILD_DIR_NAME/debug/codecov/default.profdata" + local -r coverage_report_path="$COVERAGE_DIR_NAME/$package_name.lcov" + + pushd_quiet "$SCRIPT_ABS_PATH/.." + mkdir -p "$COVERAGE_DIR_NAME" + # shellcheck disable=SC2068 + xcrun llvm-cov export -format="lcov" "$bin_path" -instr-profile "$profdata_path" ${COVERAGE_INCLUDE_LIST[@]} \ + > "$coverage_report_path" + echo "Code coverage report: $(pwd -P)/$coverage_report_path" + popd_quiet + + echo "---------------------- END COVERAGE DATA -----------------------" +} + +################### +# ENTRY POINT # +################### + +trap clean_up EXIT + +set_up + +swift_test +export_coverage_data diff --git a/Scripts/upgrade_bash.sh b/Scripts/upgrade_bash.sh new file mode 100755 index 0000000..0db3d06 --- /dev/null +++ b/Scripts/upgrade_bash.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -Eeuo pipefail + +readonly LOGIN_SHELLS_FILE_PATH="/etc/shells" + +readonly HOMEBREW_BASH_FORMULA="bash" +readonly HOMEBREW_BASH_X86_INSTALL_DIR="/usr/local/bin/bash" +readonly HOMEBREW_BASH_ARM_INSTALL_DIR="/opt/homebrew/bin/bash" + +if ! command -v "brew" &>/dev/null +then + echo "Error: 'brew' command not found. Please install Homebrew before running this script." 1>&2 + exit 1 +fi + +echo "-------------------------------- UPGRADE BASH --------------------------------" + +if ! brew list $HOMEBREW_BASH_FORMULA &>/dev/null +then + echo "Installing Bash โš™๏ธ" + brew install $HOMEBREW_BASH_FORMULA > /dev/null +else + echo "Updating Bash โš™๏ธ" + brew upgrade $HOMEBREW_BASH_FORMULA > /dev/null +fi + +if [[ -f "$HOMEBREW_BASH_ARM_INSTALL_DIR" ]] +then + HOMEBREW_BASH_INSTALL_DIR="$HOMEBREW_BASH_ARM_INSTALL_DIR" +else + HOMEBREW_BASH_INSTALL_DIR="$HOMEBREW_BASH_X86_INSTALL_DIR" +fi + +if ! grep -q $HOMEBREW_BASH_INSTALL_DIR $LOGIN_SHELLS_FILE_PATH +then + echo $HOMEBREW_BASH_INSTALL_DIR | sudo tee -a $LOGIN_SHELLS_FILE_PATH +fi + +sudo chsh -s $HOMEBREW_BASH_INSTALL_DIR + +echo -e "\n$(bash --version)" + +echo "------------------------------ END UPGRADE BASH ------------------------------" diff --git a/Sources/ConfidentialCore/Coding/DataEncoder.swift b/Sources/ConfidentialCore/Coding/DataEncoder.swift new file mode 100644 index 0000000..56528d8 --- /dev/null +++ b/Sources/ConfidentialCore/Coding/DataEncoder.swift @@ -0,0 +1,7 @@ +import Foundation + +protocol DataEncoder { + func encode(_ value: E) throws -> Data +} + +extension JSONEncoder: DataEncoder {} diff --git a/Sources/ConfidentialCore/Constants/Constants.swift b/Sources/ConfidentialCore/Constants/Constants.swift new file mode 100644 index 0000000..888fa78 --- /dev/null +++ b/Sources/ConfidentialCore/Constants/Constants.swift @@ -0,0 +1,27 @@ +enum C { + + enum Code { + + enum Format { + static let indentWidth: Int = 4 + } + + enum Generation { + static let deobfuscateDataFuncName: String = "deobfuscateData" + static let deobfuscateDataFuncParamName: String = "data" + } + } + + enum Parsing { + + enum Keywords { + static let compress: String = "compress" + static let encrypt: String = "encrypt" + static let shuffle: String = "shuffle" + static let using: String = "using" + static let create: String = "create" + static let extend: String = "extend" + static let from: String = "from" + } + } +} diff --git a/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+CaseIterable.swift b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+CaseIterable.swift new file mode 100644 index 0000000..84c520a --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+CaseIterable.swift @@ -0,0 +1,13 @@ +import ConfidentialKit + +extension Obfuscation.Compression.CompressionAlgorithm: CaseIterable { + + public static var allCases: [Self] { + [ + .lzfse, + .lz4, + .lzma, + .zlib + ] + } +} diff --git a/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+CustomStringConvertible.swift b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+CustomStringConvertible.swift new file mode 100644 index 0000000..f7ca393 --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+CustomStringConvertible.swift @@ -0,0 +1,19 @@ +import ConfidentialKit + +extension Obfuscation.Compression.CompressionAlgorithm: CustomStringConvertible { + + public var description: String { + switch self { + case .lzfse: + return "lzfse" + case .lz4: + return "lz4" + case .lzma: + return "lzma" + case .zlib: + return "zlib" + @unknown default: + return "unknown" + } + } +} diff --git a/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+Name.swift b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+Name.swift new file mode 100644 index 0000000..861b19b --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Compression/CompressionAlgorithm+Name.swift @@ -0,0 +1,19 @@ +import ConfidentialKit + +extension Obfuscation.Compression.CompressionAlgorithm { + + var name: String { + switch self { + case .lzfse: + return "lzfse" + case .lz4: + return "lz4" + case .lzma: + return "lzma" + case .zlib: + return "zlib" + @unknown default: + return "unknown" + } + } +} diff --git a/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+CaseIterable.swift b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+CaseIterable.swift new file mode 100644 index 0000000..c56dff7 --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+CaseIterable.swift @@ -0,0 +1,13 @@ +import ConfidentialKit + +extension Obfuscation.Encryption.SymmetricEncryptionAlgorithm: CaseIterable { + + public static var allCases: [Self] { + [ + .aes128GCM, + .aes192GCM, + .aes256GCM, + .chaChaPoly + ] + } +} diff --git a/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+CustomStringConvertible.swift b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+CustomStringConvertible.swift new file mode 100644 index 0000000..0d781d9 --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+CustomStringConvertible.swift @@ -0,0 +1,17 @@ +import ConfidentialKit + +extension Obfuscation.Encryption.SymmetricEncryptionAlgorithm: CustomStringConvertible { + + public var description: String { + switch self { + case .aes128GCM: + return "aes-128-gcm" + case .aes192GCM: + return "aes-192-gcm" + case .aes256GCM: + return "aes-256-gcm" + case .chaChaPoly: + return "chacha20-poly" + } + } +} diff --git a/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+Name.swift b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+Name.swift new file mode 100644 index 0000000..004ac74 --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/ConfidentialKit/Obfuscation/Encryption/SymmetricEncryptionAlgorithm+Name.swift @@ -0,0 +1,6 @@ +import ConfidentialKit + +extension Obfuscation.Encryption.SymmetricEncryptionAlgorithm { + + var name: String { rawValue } +} diff --git a/Sources/ConfidentialCore/Extensions/Foundation/Data/Data+HexString.swift b/Sources/ConfidentialCore/Extensions/Foundation/Data/Data+HexString.swift new file mode 100644 index 0000000..17176bd --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/Foundation/Data/Data+HexString.swift @@ -0,0 +1,20 @@ +import Foundation + +extension Data { + + struct HexEncodingOptions: OptionSet { + static let upperCase: Self = .init(rawValue: 1 << 0) + static let numericLiteral: Self = .init(rawValue: 1 << 1) + + let rawValue: Int + } + + func hexEncodedStringComponents(options: HexEncodingOptions = []) -> [String] { + var format = options.contains(.upperCase) ? "%02hhX" : "%02hhx" + if options.contains(.numericLiteral) { + format = "0x\(format)" + } + + return map { String(format: format, $0) } + } +} diff --git a/Sources/ConfidentialCore/Extensions/Swift/Encodable/Encodable+TypeErasure.swift b/Sources/ConfidentialCore/Extensions/Swift/Encodable/Encodable+TypeErasure.swift new file mode 100644 index 0000000..294a91c --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/Swift/Encodable/Encodable+TypeErasure.swift @@ -0,0 +1,19 @@ +struct AnyEncodable: Encodable { + + private let encode: (Encoder) throws -> Void + + init(_ encodable: any Encodable) { + self.encode = encodable.encode(to:) + } + + func encode(to encoder: Encoder) throws { + try encode(encoder) + } +} + +extension Encodable { + + func eraseToAnyEncodable() -> AnyEncodable { + .init(self) + } +} diff --git a/Sources/ConfidentialCore/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+SecureRandom.swift b/Sources/ConfidentialCore/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+SecureRandom.swift new file mode 100644 index 0000000..1816dc9 --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+SecureRandom.swift @@ -0,0 +1,26 @@ +import Foundation + +extension FixedWidthInteger { + + typealias SecureRandomNumberSource = (inout [UInt8]) -> OSStatus + + static func secureRandom( + using source: SecureRandomNumberSource = { + SecRandomCopyBytes(kSecRandomDefault, $0.count, &$0) + } + ) throws -> Self { + let count = MemoryLayout.stride + var bytes = [UInt8](repeating: .zero, count: count) + let status = source(&bytes) + guard status == errSecSuccess else { + let errorDescription = SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error" + throw NSError( + domain: NSOSStatusErrorDomain, + code: .init(status), + userInfo: [NSLocalizedDescriptionKey: errorDescription] + ) + } + + return bytes.withUnsafeBytes { $0.load(as: Self.self) } + } +} diff --git a/Sources/ConfidentialCore/Extensions/SwiftSyntax/TokenSyntax/TokenSyntax+Tokens.swift b/Sources/ConfidentialCore/Extensions/SwiftSyntax/TokenSyntax/TokenSyntax+Tokens.swift new file mode 100644 index 0000000..c8aac9a --- /dev/null +++ b/Sources/ConfidentialCore/Extensions/SwiftSyntax/TokenSyntax/TokenSyntax+Tokens.swift @@ -0,0 +1,79 @@ +import SwiftSyntax + +extension TokenSyntax { + + static func atSign( + leadingNewlines: Int, + followedByLeadingSpaces leadingSpaces: Int = C.Code.Format.indentWidth + ) -> Self { + makeToken( + .atSign.withoutTrivia(), + withLeadingNewlines: leadingNewlines, + followedByLeadingSpaces: leadingSpaces + ) + } + + static func period( + leadingNewlines: Int, + followedByLeadingSpaces leadingSpaces: Int = C.Code.Format.indentWidth + ) -> Self { + makeToken( + .period.withoutTrivia(), + withLeadingNewlines: leadingNewlines, + followedByLeadingSpaces: leadingSpaces + ) + } + + static func rightParen( + leadingNewlines: Int, + followedByLeadingSpaces leadingSpaces: Int = C.Code.Format.indentWidth + ) -> Self { + makeToken( + .rightParen.withoutTrivia(), + withLeadingNewlines: leadingNewlines, + followedByLeadingSpaces: leadingSpaces + ) + } + + static func `private`( + leadingNewlines: Int, + followedByLeadingSpaces leadingSpaces: Int = C.Code.Format.indentWidth, + trailingSpaces: Int = 1 + ) -> Self { + makeToken( + .private.withoutTrivia().withTrailingTrivia(.spaces(trailingSpaces)), + withLeadingNewlines: leadingNewlines, + followedByLeadingSpaces: leadingSpaces + ) + } + + static func `static`( + leadingNewlines: Int, + followedByLeadingSpaces leadingSpaces: Int = C.Code.Format.indentWidth, + trailingSpaces: Int = 1 + ) -> Self { + makeToken( + .static.withoutTrivia().withTrailingTrivia(.spaces(trailingSpaces)), + withLeadingNewlines: leadingNewlines, + followedByLeadingSpaces: leadingSpaces + ) + } +} + +private extension TokenSyntax { + + static func makeToken( + _ token: Self, + withLeadingNewlines leadingNewlines: Int, + followedByLeadingSpaces leadingSpaces: Int + ) -> Self { + guard leadingNewlines > .zero else { + return token + } + + return token.withLeadingTrivia( + .newlines(leadingNewlines) + .appending(.spaces(leadingSpaces)) + ) + } +} diff --git a/Sources/ConfidentialCore/Models/Configuration/Configuration.Secret.Value+UnderlyingValue.swift b/Sources/ConfidentialCore/Models/Configuration/Configuration.Secret.Value+UnderlyingValue.swift new file mode 100644 index 0000000..25d66a7 --- /dev/null +++ b/Sources/ConfidentialCore/Models/Configuration/Configuration.Secret.Value+UnderlyingValue.swift @@ -0,0 +1,11 @@ +extension Configuration.Secret.Value { + + var underlyingValue: AnyEncodable { + switch self { + case let .array(value): + return value.eraseToAnyEncodable() + case let .singleValue(value): + return value.eraseToAnyEncodable() + } + } +} diff --git a/Sources/ConfidentialCore/Models/Configuration/Configuration.swift b/Sources/ConfidentialCore/Models/Configuration/Configuration.swift new file mode 100644 index 0000000..fabf4f4 --- /dev/null +++ b/Sources/ConfidentialCore/Models/Configuration/Configuration.swift @@ -0,0 +1,65 @@ +import ConfidentialKit + +public struct Configuration: Equatable, Decodable { + var algorithm: ArraySlice + var defaultNamespace: String? + var secrets: ArraySlice + + init( + algorithm: ArraySlice, + defaultNamespace: String?, + secrets: ArraySlice + ) { + self.algorithm = algorithm + self.defaultNamespace = defaultNamespace + self.secrets = secrets + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self = .init( + algorithm: try container.decode([String].self, forKey: .algorithm)[...], + defaultNamespace: try? container.decodeIfPresent(String.self, forKey: .defaultNamespace), + secrets: try container.decode([Secret].self, forKey: .secrets)[...] + ) + } +} + +extension Configuration { + + struct Secret: Equatable, Hashable, Decodable { + let name: String + let value: Value + let namespace: String? + } +} + +extension Configuration.Secret { + + enum Value: Equatable, Hashable, Decodable { + + typealias DataTypes = Obfuscation.SupportedDataTypes + + case array(DataTypes.Array) + case singleValue(DataTypes.SingleValue) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let value = try? container.decode(DataTypes.Array.self) { + self = .array(value) + } else { + let value = try container.decode(DataTypes.SingleValue.self) + self = .singleValue(value) + } + } + } +} + +private extension Configuration { + + enum CodingKeys: String, CodingKey { + case algorithm + case defaultNamespace + case secrets + } +} diff --git a/Sources/ConfidentialCore/Models/SourceSpecification/SourceSpecification.swift b/Sources/ConfidentialCore/Models/SourceSpecification/SourceSpecification.swift new file mode 100644 index 0000000..62a490b --- /dev/null +++ b/Sources/ConfidentialCore/Models/SourceSpecification/SourceSpecification.swift @@ -0,0 +1,108 @@ +import ConfidentialKit +import Foundation + +public struct SourceSpecification: Equatable { + var algorithm: Algorithm + var secrets: Secrets +} + +public extension SourceSpecification { + + typealias Algorithm = ArraySlice + + struct ObfuscationStep: Equatable { + let technique: Technique + } + + struct Secret { + let name: String + var data: Data + let dataAccessWrapperInfo: DataAccessWrapperInfo + } + + struct Secrets: Equatable, Hashable { + + public typealias Secret = SourceSpecification.Secret + + private var secrets: [Secret.Namespace: ArraySlice] + + var namespaces: Dictionary>.Keys { + secrets.keys + } + + init(_ secrets: [Secret.Namespace: ArraySlice]) { + self.secrets = secrets + } + + subscript(namespace: Secret.Namespace) -> ArraySlice? { + _read { yield self.secrets[namespace] } + _modify { yield &self.secrets[namespace] } + } + } +} + +public extension SourceSpecification.ObfuscationStep { + + enum Technique: Equatable, Hashable { + case compression(algorithm: Obfuscation.Compression.CompressionAlgorithm) + case encryption(algorithm: Obfuscation.Encryption.SymmetricEncryptionAlgorithm) + case randomization(nonce: UInt64) + } +} + +extension SourceSpecification.Secret: Equatable, Hashable { + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.name == rhs.name && lhs.data == rhs.data + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(name) + hasher.combine(data) + } +} + +extension SourceSpecification.Secret { + + struct DataAccessWrapperInfo { + + typealias Argument = (label: String?, value: String) + + let typeInfo: TypeInfo + let arguments: [Argument] + } + + public enum Namespace: Equatable, Hashable { + case create(identifier: String) + case extend(identifier: String, moduleName: String? = nil) + } +} + +extension SourceSpecification.Secrets: Collection { + + public typealias Element = Dictionary>.Element + public typealias Index = Dictionary>.Index + + public var startIndex: Index { + secrets.startIndex + } + + public var endIndex: Index { + secrets.endIndex + } + + public subscript(position: Index) -> Element { + secrets[position] + } + + public func index(after index: Index) -> Index { + secrets.index(after: index) + } +} + +extension SourceSpecification.Secrets: ExpressibleByDictionaryLiteral { + + public init(dictionaryLiteral elements: (Secret.Namespace, ArraySlice)...) { + self.init(.init(uniqueKeysWithValues: elements)) + } +} diff --git a/Sources/ConfidentialCore/Obfuscation/DataObfuscationStep.swift b/Sources/ConfidentialCore/Obfuscation/DataObfuscationStep.swift new file mode 100644 index 0000000..f0742ac --- /dev/null +++ b/Sources/ConfidentialCore/Obfuscation/DataObfuscationStep.swift @@ -0,0 +1,5 @@ +import Foundation + +protocol DataObfuscationStep { + func obfuscate(_ data: Data) throws -> Data +} diff --git a/Sources/ConfidentialCore/Obfuscation/SourceObfuscator+ObfuscationStepResolver.swift b/Sources/ConfidentialCore/Obfuscation/SourceObfuscator+ObfuscationStepResolver.swift new file mode 100644 index 0000000..d4ca477 --- /dev/null +++ b/Sources/ConfidentialCore/Obfuscation/SourceObfuscator+ObfuscationStepResolver.swift @@ -0,0 +1,33 @@ +import ConfidentialKit + +protocol DataObfuscationStepResolver { + + typealias Technique = SourceSpecification.ObfuscationStep.Technique + + func obfuscationStep(for technique: Technique) -> any DataObfuscationStep +} + +extension SourceObfuscator { + + struct ObfuscationStepResolver: DataObfuscationStepResolver { + + @inline(__always) + func obfuscationStep(for technique: Technique) -> any DataObfuscationStep { + switch technique { + case let .compression(algorithm): + return Obfuscation.Compression.DataCompressor(algorithm: algorithm) + case let .encryption(algorithm): + return Obfuscation.Encryption.DataCrypter(algorithm: algorithm) + case let .randomization(nonce): + return Obfuscation.Randomization.DataShuffler(nonce: nonce) + } + } + } +} + +public extension SourceObfuscator { + + init() { + self.init(obfuscationStepResolver: ObfuscationStepResolver()) + } +} diff --git a/Sources/ConfidentialCore/Obfuscation/SourceObfuscator.swift b/Sources/ConfidentialCore/Obfuscation/SourceObfuscator.swift new file mode 100644 index 0000000..13da7a9 --- /dev/null +++ b/Sources/ConfidentialCore/Obfuscation/SourceObfuscator.swift @@ -0,0 +1,47 @@ +import Foundation + +public struct SourceObfuscator { + + private let obfuscationStepResolver: DataObfuscationStepResolver + + init(obfuscationStepResolver: DataObfuscationStepResolver) { + self.obfuscationStepResolver = obfuscationStepResolver + } + + public func obfuscate(_ source: inout SourceSpecification) throws { + guard !source.secrets.isEmpty else { + return + } + + let obfuscateData = obfuscationFunc(given: source.algorithm) + try source.secrets.namespaces.forEach { namespace in + guard let secrets = source.secrets[namespace] else { + fatalError("Unexpected source specification integrity violation") + } + + source.secrets[namespace] = try secrets.map { secret in + var secret = secret + secret.data = try obfuscateData(secret.data) + return secret + }[...] + } + } +} + +private extension SourceObfuscator { + + typealias Algorithm = SourceSpecification.Algorithm + typealias ObfuscationFunc = (Data) throws -> Data + + @inline(__always) + func obfuscationFunc(given algorithm: Algorithm) -> ObfuscationFunc { + algorithm + .map(\.technique) + .map(obfuscationStepResolver.obfuscationStep(for:)) + .reduce({ $0 }) { partialFunc, step in + return { + try step.obfuscate(partialFunc($0)) + } + } + } +} diff --git a/Sources/ConfidentialCore/Obfuscation/Techniques/Compression/DataCompressor+DataObfuscationStep.swift b/Sources/ConfidentialCore/Obfuscation/Techniques/Compression/DataCompressor+DataObfuscationStep.swift new file mode 100644 index 0000000..d869740 --- /dev/null +++ b/Sources/ConfidentialCore/Obfuscation/Techniques/Compression/DataCompressor+DataObfuscationStep.swift @@ -0,0 +1,10 @@ +import ConfidentialKit +import Foundation + +extension Obfuscation.Compression.DataCompressor: DataObfuscationStep { + + func obfuscate(_ data: Data) throws -> Data { + let compressedData = try NSData(data: data).compressed(using: algorithm) + return .init(referencing: compressedData) + } +} diff --git a/Sources/ConfidentialCore/Obfuscation/Techniques/Encryption/DataCrypter+DataObfuscationStep.swift b/Sources/ConfidentialCore/Obfuscation/Techniques/Encryption/DataCrypter+DataObfuscationStep.swift new file mode 100644 index 0000000..7284484 --- /dev/null +++ b/Sources/ConfidentialCore/Obfuscation/Techniques/Encryption/DataCrypter+DataObfuscationStep.swift @@ -0,0 +1,32 @@ +import ConfidentialKit +import CryptoKit +import Foundation + +extension Obfuscation.Encryption.DataCrypter: DataObfuscationStep { + + func obfuscate(_ data: Data) throws -> Data { + let key = SymmetricKey(size: algorithm.keySize) + + var obfuscatedData: Data + switch algorithm { + case .aes128GCM, .aes192GCM, .aes256GCM: + let sealedBox = try AES.GCM.seal(data, using: key, nonce: .init()) + /* + As the official documentation states, when we use the nonce of the + default size of 12 bytes, the combined representation is available, + hence the use of force unwrap. + See https://developer.apple.com/documentation/cryptokit/aes/gcm/sealedbox + for more details + */ + obfuscatedData = sealedBox.combined! + case .chaChaPoly: + let sealedBox = try ChaChaPoly.seal(data, using: key, nonce: .init()) + obfuscatedData = sealedBox.combined + } + + let keyData = key.withUnsafeBytes(Data.init(_:)) + obfuscatedData.append(keyData) + + return obfuscatedData + } +} diff --git a/Sources/ConfidentialCore/Obfuscation/Techniques/Randomization/DataShuffler+DataObfuscationStep.swift b/Sources/ConfidentialCore/Obfuscation/Techniques/Randomization/DataShuffler+DataObfuscationStep.swift new file mode 100644 index 0000000..b051317 --- /dev/null +++ b/Sources/ConfidentialCore/Obfuscation/Techniques/Randomization/DataShuffler+DataObfuscationStep.swift @@ -0,0 +1,62 @@ +import ConfidentialKit +import Foundation + +extension Obfuscation.Randomization.DataShuffler: DataObfuscationStep { + + func obfuscate(_ data: Data) throws -> Data { + let shuffledIndexes: [Int] = (0.. [UInt8] { + var result: [UInt8] = .init(repeating: .zero, count: bytes.count) + indexes.enumerated().forEach { oldIdx, newIdx in + result[newIdx] = bytes[oldIdx] + } + + return result + } + + @inline(__always) + func obfuscateIndexes(_ indexes: [Int]) -> (bytes: [UInt8], byteWidth: UInt8) { + let highestIndex = indexes.count - 1 + switch highestIndex { + case _ where highestIndex <= UInt8.max: + return ( + bytes: obfuscateIndexes(indexes, indexTransform: UInt8.init), + byteWidth: .init(UInt8.byteWidth) + ) + case _ where highestIndex <= UInt16.max: + return ( + bytes: obfuscateIndexes(indexes, indexTransform: UInt16.init), + byteWidth: .init(UInt16.byteWidth) + ) + default: + return ( + bytes: obfuscateIndexes(indexes, indexTransform: { $0 }), + byteWidth: .init(Int.byteWidth) + ) + } + } + + @inline(__always) + func obfuscateIndexes(_ indexes: [Int], indexTransform: (Int) -> I) -> [UInt8] { + indexes + .map(indexTransform) + .flatMap { withUnsafeBytes(of: $0 ^ .init(bytes: nonce.bytes), [UInt8].init) } + } +} diff --git a/Sources/ConfidentialCore/Parsing/Builders/Variadics.swift b/Sources/ConfidentialCore/Parsing/Builders/Variadics.swift new file mode 100644 index 0000000..cb60177 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Builders/Variadics.swift @@ -0,0 +1,38 @@ +import Parsing +import SwiftSyntaxBuilder + +public struct ExpressibleAsCodeBlockItemFlatMap: Parser +where +P.Input == SourceSpecification, +P.Output == [ExpressibleAsCodeBlockItem] +{ + + private let parsers: [P] + + init(_ parsers: [P]) { + self.parsers = parsers + } + + public func parse(_ input: inout SourceSpecification) throws -> [ExpressibleAsCodeBlockItem] { + try parsers.flatMap { parser in + try parser.parse(&input) + } + } +} + +extension ParserBuilder { + + static func buildBlock

(_ parsers: P...) -> ExpressibleAsCodeBlockItemFlatMap

{ + .init(parsers) + } +} + +extension OneOfBuilder { + + static func buildBlock

(_ parsers: P...) -> Parsers.OneOfMany

+ where + P.Input == Substring, + P.Output == SourceSpecification.ObfuscationStep.Technique { + .init(parsers) + } +} diff --git a/Sources/ConfidentialCore/Parsing/ConvenienceTypealiases.swift b/Sources/ConfidentialCore/Parsing/ConvenienceTypealiases.swift new file mode 100644 index 0000000..4f1880c --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/ConvenienceTypealiases.swift @@ -0,0 +1,3 @@ +import Parsing + +public typealias Parsers = Parsing.Parsers diff --git a/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/DeobfuscateDataFunctionDeclParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/DeobfuscateDataFunctionDeclParser.swift new file mode 100644 index 0000000..ce21ca3 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/DeobfuscateDataFunctionDeclParser.swift @@ -0,0 +1,130 @@ +import Parsing +import SwiftSyntaxBuilder + +struct DeobfuscateDataFunctionDeclParser: Parser { + + typealias Algorithm = SourceSpecification.Algorithm + + private let functionNestingLevel: UInt8 + + init(functionNestingLevel: UInt8) { + self.functionNestingLevel = functionNestingLevel + } + + func parse(_ input: inout Algorithm) throws -> ExpressibleAsMemberDeclListItem { + var obfuscationStepsCount = input.count + guard obfuscationStepsCount > .zero else { + throw ParsingError.assertionFailed( + description: "Obfuscation algorithm must consist of at least one obfuscation step." + ) + } + + let reversedAlgorithm = input.reversed() + let innerMostObfuscationStep = try obfuscationStepExpr( + for: reversedAlgorithm[reversedAlgorithm.startIndex], + indentWidthMultiplier: obfuscationStepsCount + ) + let bodyExpr = try reversedAlgorithm + .dropFirst() + .reduce(innerMostObfuscationStep) { innerExpr, step in + obfuscationStepsCount -= 1 + return try obfuscationStepExpr( + for: step, + withInnerExpr: innerExpr, + indentWidthMultiplier: obfuscationStepsCount + ) + } + + input.removeAll() + + return DeobfuscateDataFunctionDecl( + declNestingLevel: functionNestingLevel, + body: bodyExpr + ) + } +} + +private extension DeobfuscateDataFunctionDeclParser { + + typealias ObfuscationStep = SourceSpecification.ObfuscationStep + + func obfuscationStepExpr( + for obfuscationStep: ObfuscationStep, + indentWidthMultiplier: Int + ) throws -> ExpressibleAsExprBuildable { + let tryIndentWidth = try exprIndentWidth(with: indentWidthMultiplier) + let calledExprIndentWidth = tryIndentWidth + C.Code.Format.indentWidth + return TryExpr( + tryKeyword: .try.withLeadingTrivia(.spaces(tryIndentWidth)), + expression: FunctionCallExpr( + calledExpression: deobfuscateFunctionAccessExpr( + for: obfuscationStep.technique, + indentWidth: calledExprIndentWidth), + leftParen: .leftParen, + rightParen: .rightParen, + argumentListBuilder: { + TupleExprElement( + expression: IdentifierExpr(C.Code.Generation.deobfuscateDataFuncParamName) + ) + } + ) + ) + } + + func obfuscationStepExpr( + for obfuscationStep: ObfuscationStep, + withInnerExpr innerExpr: ExpressibleAsExprBuildable, + indentWidthMultiplier: Int + ) throws -> ExpressibleAsExprBuildable { + let tryIndentWidth = try exprIndentWidth(with: indentWidthMultiplier) + let calledExprIndentWidth = tryIndentWidth + C.Code.Format.indentWidth + return TryExpr( + tryKeyword: .try.withLeadingTrivia(.spaces(tryIndentWidth)), + expression: FunctionCallExpr( + calledExpression: deobfuscateFunctionAccessExpr( + for: obfuscationStep.technique, + indentWidth: calledExprIndentWidth + ), + leftParen: .leftParen.withTrailingTrivia(.newlines(1)), + rightParen: .rightParen( + leadingNewlines: 1, + followedByLeadingSpaces: calledExprIndentWidth + ), + argumentListBuilder: { + TupleExprElement(expression: innerExpr) + } + ) + ) + } + + func deobfuscateFunctionAccessExpr( + for technique: ObfuscationStep.Technique, + indentWidth: Int + ) -> ExpressibleAsExprBuildable { + let initCallExpr: ExpressibleAsExprBuildable + switch technique { + case let .compression(algorithm): + initCallExpr = DataCompressorInitializerCallExpr(compressionAlgorithm: algorithm) + case let .encryption(algorithm): + initCallExpr = DataCrypterInitializerCallExpr(encryptionAlgorithm: algorithm) + case let .randomization(nonce): + initCallExpr = DataShufflerInitializerCallExpr(nonce: nonce) + } + + return DeobfuscateFunctionAccessExpr(initCallExpr, dotIndentWidth: indentWidth) + } +} + +private extension DeobfuscateDataFunctionDeclParser { + + func exprIndentWidth(with indentWidthMultiplier: Int) throws -> Int { + guard indentWidthMultiplier > .zero else { + throw ParsingError.assertionFailed( + description: "Indent width multiplier must be greater than zero." + ) + } + let multiplier = 1 + Int(functionNestingLevel) + (indentWidthMultiplier - 1) * 2 + + return multiplier * C.Code.Format.indentWidth + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/NamespaceDeclParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/NamespaceDeclParser.swift new file mode 100644 index 0000000..ea04659 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/NamespaceDeclParser.swift @@ -0,0 +1,60 @@ +import Parsing +import SwiftSyntaxBuilder + +struct NamespaceDeclParser: Parser +where +MembersParser.Input == ArraySlice, +MembersParser.Output == [ExpressibleAsMemberDeclListItem], +DeobfuscateDataFunctionDeclParser.Input == SourceSpecification.Algorithm, +DeobfuscateDataFunctionDeclParser.Output == ExpressibleAsMemberDeclListItem +{ + + private let membersParser: MembersParser + private let deobfuscateDataFunctionDeclParser: DeobfuscateDataFunctionDeclParser + + init( + membersParser: MembersParser, + deobfuscateDataFunctionDeclParser: DeobfuscateDataFunctionDeclParser + ) { + self.membersParser = membersParser + self.deobfuscateDataFunctionDeclParser = deobfuscateDataFunctionDeclParser + } + + func parse(_ input: inout SourceSpecification) throws -> [ExpressibleAsCodeBlockItem] { + let deobfuscateDataFunctionDecl = try deobfuscateDataFunctionDeclParser.parse(&input.algorithm) + let codeBlocks = try input.secrets.namespaces + .map { namespace -> ExpressibleAsCodeBlockItem in + guard var secrets = input.secrets[namespace] else { + fatalError("Unexpected source specification integrity violation") + } + + var declarations = try membersParser.parse(&secrets) + declarations.append(deobfuscateDataFunctionDecl) + let members = MemberDeclBlock( + leftBrace: .leftBrace.withLeadingTrivia(.spaces(1)), + members: MemberDeclList(declarations) + ) + let decl: ExpressibleAsCodeBlockItem + switch namespace { + case let .create(identifier): + decl = EnumDecl( + enumKeyword: .enum.withLeadingTrivia(.newlines(1)), + identifier: identifier, + members: members + ) + case let .extend(identifier, _): + decl = ExtensionDecl( + modifiers: .none, + extensionKeyword: .extension.withLeadingTrivia(.newlines(1)), + extendedType: SimpleTypeIdentifier(identifier), + members: members + ) + } + input.secrets[namespace] = secrets.isEmpty ? nil : secrets + + return decl + } + + return codeBlocks + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/NamespaceMembersParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/NamespaceMembersParser.swift new file mode 100644 index 0000000..e035426 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/NamespaceMembersParser.swift @@ -0,0 +1,34 @@ +import Parsing +import SwiftSyntaxBuilder + +struct NamespaceMembersParser: Parser +where +SecretDeclParser.Input == SourceSpecification.Secret, +SecretDeclParser.Output == ExpressibleAsSecretDecl +{ + + private let secretDeclParser: SecretDeclParser + + init(secretDeclParser: SecretDeclParser) { + self.secretDeclParser = secretDeclParser + } + + func parse(_ input: inout ArraySlice) throws -> [ExpressibleAsMemberDeclListItem] { + var parsedSecretsCount: Int = .zero + let declarations: [SecretDecl] + do { + declarations = try input.map { secret in + let decl = try secretDeclParser.parse(secret).createSecretDecl() + parsedSecretsCount += 1 + + return decl + } + input.removeFirst(parsedSecretsCount) + } catch let error { + input.removeFirst(parsedSecretsCount) + throw error + } + + return declarations + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/Parsers+CodeGeneration.swift b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/Parsers+CodeGeneration.swift new file mode 100644 index 0000000..f8b66df --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/Parsers+CodeGeneration.swift @@ -0,0 +1,4 @@ +public extension Parsers { + + enum CodeGeneration {} +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/SecretDeclParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/SecretDeclParser.swift new file mode 100644 index 0000000..b0b5347 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/SecretDeclParser.swift @@ -0,0 +1,47 @@ +import Parsing +import SwiftSyntaxBuilder + +struct SecretDeclParser: Parser { + + typealias Secret = SourceSpecification.Secret + + func parse(_ input: inout Secret) throws -> ExpressibleAsSecretDecl { + let valueHexComponents = input.data.hexEncodedStringComponents(options: .numericLiteral) + let dataArgumentElements = valueHexComponents + .enumerated() + .map { idx, component in + ArrayElement( + expression: IntegerLiteralExpr(digits: component), + trailingComma: idx < valueHexComponents.endIndex - 1 ? .comma : .none + ) + } + + return SecretDecl( + name: input.name, + dataArgumentExpression: ArrayExpr(elements: ArrayElementList(dataArgumentElements)), + dataAccessWrapper: dataAccessWrapper(with: input.dataAccessWrapperInfo) + ) + } +} + +private extension SecretDeclParser { + + func dataAccessWrapper(with wrapperInfo: Secret.DataAccessWrapperInfo) -> ExpressibleAsCustomAttribute { + CustomAttribute( + atSignToken: .atSign(leadingNewlines: 1), + attributeName: SimpleTypeIdentifier(wrapperInfo.typeInfo.fullyQualifiedName), + leftParen: .leftParen, + argumentList: TupleExprElementList( + wrapperInfo.arguments + .map { argument in + TupleExprElement( + label: argument.label.map { .identifier($0) }, + colon: argument.label.map { _ in .colon }, + expression: IdentifierExpr(argument.value) + ) + } + ), + rightParen: .rightParen + ) + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/SourceFileParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/SourceFileParser.swift new file mode 100644 index 0000000..b48bbb8 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/CodeGeneration/SourceFileParser.swift @@ -0,0 +1,66 @@ +import ConfidentialKit +import Foundation +import Parsing +import SwiftSyntaxBuilder + +public struct SourceFileParser: Parser +where +CodeBlockParsers.Input == SourceSpecification, +CodeBlockParsers.Output == [ExpressibleAsCodeBlockItem] +{ + + private let codeBlockParsers: CodeBlockParsers + + init(@ParserBuilder with build: () -> CodeBlockParsers) { + self.codeBlockParsers = build() + } + + public func parse(_ input: inout SourceSpecification) throws -> SourceFileText { + var statements = Self.makeModuleImportList(from: input.secrets.namespaces) + .enumerated() + .map { idx, moduleName -> ExpressibleAsCodeBlockItem in + ImportDecl( + path: AccessPath([AccessPathComponent(name: .identifier(moduleName))]) + ) + } + statements.append( + contentsOf: try codeBlockParsers.parse(&input) + ) + + return .init(from: SourceFile( + statements: CodeBlockItemList(statements), + eofToken: .eof + )) + } +} + +private extension SourceFileParser { + + static func makeModuleImportList(from namespaces: Namespaces) -> [String] + where + Namespaces.Element == SourceSpecification.Secret.Namespace + { + let confidentialKitModuleName = TypeInfo(of: Obfuscation.self).moduleName + let foundationModuleName = TypeInfo(of: Data.self).moduleName + let defaultModuleNames = [confidentialKitModuleName, foundationModuleName] + let moduleNames = defaultModuleNames + namespaces + .compactMap { namespace -> String? in + guard + case let .extend(_, moduleName) = namespace, + let moduleName = moduleName, + !defaultModuleNames.contains(moduleName) + else { + return nil + } + + return moduleName + } + + return moduleNames.sorted() + } +} + +public extension Parsers.CodeGeneration { + + typealias SourceFile = SourceFileParser +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/AlgorithmParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/AlgorithmParser.swift new file mode 100644 index 0000000..5b78041 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/AlgorithmParser.swift @@ -0,0 +1,34 @@ +import Parsing + +struct AlgorithmParser: Parser +where +ObfuscationStepParser.Input == Substring, +ObfuscationStepParser.Output == SourceSpecification.ObfuscationStep +{ + typealias Algorithm = SourceSpecification.Algorithm + + private let obfuscationStepParser: ObfuscationStepParser + + init(obfuscationStepParser: ObfuscationStepParser) { + self.obfuscationStepParser = obfuscationStepParser + } + + func parse(_ input: inout Configuration) throws -> Algorithm { + var parsedObfuscationStepsCount: Int = .zero + let output: [ObfuscationStepParser.Output] + do { + output = try input.algorithm.reduce(into: .init(), { steps, step in + let obfuscationStep = try obfuscationStepParser.parse(step) + parsedObfuscationStepsCount += 1 + + steps.append(obfuscationStep) + }) + input.algorithm.removeFirst(parsedObfuscationStepsCount) + } catch let error { + input.algorithm.removeFirst(parsedObfuscationStepsCount) + throw error + } + + return output[...] + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/CompressionTechniqueParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/CompressionTechniqueParser.swift new file mode 100644 index 0000000..c8dc0b6 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/CompressionTechniqueParser.swift @@ -0,0 +1,29 @@ +import ConfidentialKit +import Parsing + +struct CompressionTechniqueParser: Parser { + + typealias Technique = SourceSpecification.ObfuscationStep.Technique + + private typealias Algorithm = Obfuscation.Compression.CompressionAlgorithm + + func parse(_ input: inout Substring) throws -> Technique { + try Parse { + Parse { + Whitespace() + C.Parsing.Keywords.compress + Whitespace(1...) + C.Parsing.Keywords.using + Whitespace(1...) + } + OneOf { + for algorithm in Algorithm.allCases { + algorithm.description.map { algorithm } + } + } + End() + }.map { + .compression(algorithm: $0) + }.parse(&input) + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/EncryptionTechniqueParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/EncryptionTechniqueParser.swift new file mode 100644 index 0000000..03207a1 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/EncryptionTechniqueParser.swift @@ -0,0 +1,29 @@ +import ConfidentialKit +import Parsing + +struct EncryptionTechniqueParser: Parser { + + typealias Technique = SourceSpecification.ObfuscationStep.Technique + + private typealias Algorithm = Obfuscation.Encryption.SymmetricEncryptionAlgorithm + + func parse(_ input: inout Substring) throws -> Technique { + try Parse { + Parse { + Whitespace() + C.Parsing.Keywords.encrypt + Whitespace(1...) + C.Parsing.Keywords.using + Whitespace(1...) + } + OneOf { + for algorithm in Algorithm.allCases { + algorithm.description.map { algorithm } + } + } + End() + }.map { + .encryption(algorithm: $0) + }.parse(&input) + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/ObfuscationStepParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/ObfuscationStepParser.swift new file mode 100644 index 0000000..6a34ecf --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/ObfuscationStepParser.swift @@ -0,0 +1,21 @@ +import Parsing + +struct ObfuscationStepParser: Parser +where +TechniqueParsers.Input == Substring, +TechniqueParsers.Output == SourceSpecification.ObfuscationStep.Technique +{ + typealias ObfuscationStep = SourceSpecification.ObfuscationStep + + private let techniqueParsers: TechniqueParsers + + init(@OneOfBuilder with build: () -> TechniqueParsers) { + self.techniqueParsers = build() + } + + func parse(_ input: inout Substring) throws -> ObfuscationStep { + try techniqueParsers + .map(ObfuscationStep.init(technique:)) + .parse(&input) + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/Parsers+ModelTransform.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/Parsers+ModelTransform.swift new file mode 100644 index 0000000..084692e --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/Parsers+ModelTransform.swift @@ -0,0 +1,4 @@ +public extension Parsers { + + enum ModelTransform {} +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/RandomizationTechniqueParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/RandomizationTechniqueParser.swift new file mode 100644 index 0000000..09bad12 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/RandomizationTechniqueParser.swift @@ -0,0 +1,23 @@ +import Parsing + +struct RandomizationTechniqueParser: Parser { + + typealias GenerateNonce = () throws -> UInt64 + typealias Technique = SourceSpecification.ObfuscationStep.Technique + + private let generateNonce: GenerateNonce + + init(generateNonce: @escaping GenerateNonce = { try UInt64.secureRandom() }) { + self.generateNonce = generateNonce + } + + func parse(_ input: inout Substring) throws -> Technique { + try Parse { + Whitespace() + C.Parsing.Keywords.shuffle + End() + }.parse(&input) + + return .randomization(nonce: try generateNonce()) + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SecretNamespaceParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SecretNamespaceParser.swift new file mode 100644 index 0000000..aa8af09 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SecretNamespaceParser.swift @@ -0,0 +1,50 @@ +import ConfidentialKit +import Parsing + +struct SecretNamespaceParser: Parser { + + typealias Namespace = SourceSpecification.Secret.Namespace + + private enum NamespaceKind: Equatable { + case create + case extend + } + + func parse(_ input: inout Substring) throws -> Namespace { + guard !input.isEmpty else { + let defaultNamespaceInfo = TypeInfo(of: Obfuscation.Secret.self) + return .extend( + identifier: defaultNamespaceInfo.fullyQualifiedName, + moduleName: defaultNamespaceInfo.moduleName + ) + } + + return try Parse { + Whitespace() + OneOf { + C.Parsing.Keywords.create.map { NamespaceKind.create } + C.Parsing.Keywords.extend.map { NamespaceKind.extend } + } + }.flatMap { namespaceKind in + Always(namespaceKind) + Whitespace() + Prefix(1...) { !$0.isWhitespace } + if case .extend = namespaceKind { + Optionally { + Whitespace() + C.Parsing.Keywords.from + Whitespace() + Prefix(1...) { !$0.isWhitespace } + } + } + End() + }.map { namespaceKind, identifier, moduleName -> Namespace in + switch namespaceKind { + case .create: + return .create(identifier: .init(identifier)) + case .extend: + return .extend(identifier: .init(identifier), moduleName: moduleName?.map(String.init)) + } + }.parse(&input) + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SecretsParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SecretsParser.swift new file mode 100644 index 0000000..c3ef27b --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SecretsParser.swift @@ -0,0 +1,79 @@ +import ConfidentialKit +import Foundation +import Parsing + +struct SecretsParser: Parser +where +NamespaceParser.Input == Substring, +NamespaceParser.Output == SourceSpecification.Secret.Namespace +{ + typealias Secrets = SourceSpecification.Secrets + + private let namespaceParser: NamespaceParser + private let secretValueEncoder: DataEncoder + + init( + namespaceParser: NamespaceParser, + secretValueEncoder: DataEncoder = JSONEncoder() + ) { + self.namespaceParser = namespaceParser + self.secretValueEncoder = secretValueEncoder + } + + func parse(_ input: inout Configuration) throws -> Secrets { + guard !input.secrets.isEmpty else { + throw ParsingError.assertionFailed(description: "`secrets` list must not be empty.") + } + + var parsedSecretsCount: Int = .zero + let output: Secrets + do { + output = try input.secrets.reduce(into: .init(), { secrets, secret in + let namespace = try namespaceParser.parse( + secret.namespace ?? input.defaultNamespace ?? "" + ) + let secret = SourceSpecification.Secret( + name: secret.name, + data: try secretValueEncoder.encode(secret.value.underlyingValue), + dataAccessWrapperInfo: dataAccessWrapperInfo(for: secret.value) + ) + parsedSecretsCount += 1 + + switch secrets[namespace]?.append(secret) { + case .none: + secrets[namespace] = [secret] + case .some: + return + } + }) + input.secrets.removeFirst(parsedSecretsCount) + } catch let error { + input.secrets.removeFirst(parsedSecretsCount) + throw error + } + + return output + } +} + +private extension SecretsParser { + + func dataAccessWrapperInfo(for value: Configuration.Secret.Value) -> SourceSpecification.Secret.DataAccessWrapperInfo { + typealias DataTypes = Configuration.Secret.Value.DataTypes + + let typeInfo: TypeInfo + switch value { + case .array: + typeInfo = .init(of: Obfuscated.self) + case .singleValue: + typeInfo = .init(of: Obfuscated.self) + } + + return .init( + typeInfo: typeInfo, + arguments: [ + (label: .none, value: C.Code.Generation.deobfuscateDataFuncName) + ] + ) + } +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SourceSpecificationParser.swift b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SourceSpecificationParser.swift new file mode 100644 index 0000000..0c25339 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ModelTransform/SourceSpecificationParser.swift @@ -0,0 +1,33 @@ +import Parsing + +public struct SourceSpecificationParser: Parser +where +AlgorithmParser.Input == Configuration, +AlgorithmParser.Output == SourceSpecification.Algorithm, +SecretsParser.Input == Configuration, +SecretsParser.Output == SourceSpecification.Secrets +{ + + private let algorithmParser: AlgorithmParser + private let secretsParser: SecretsParser + + init(algorithmParser: AlgorithmParser, secretsParser: SecretsParser) { + self.algorithmParser = algorithmParser + self.secretsParser = secretsParser + } + + public func parse(_ input: inout Configuration) throws -> SourceSpecification { + let spec = SourceSpecification( + algorithm: try algorithmParser.parse(&input), + secrets: try secretsParser.parse(&input) + ) + input.defaultNamespace = nil + + return spec + } +} + +public extension Parsers.ModelTransform { + + typealias SourceSpecification = SourceSpecificationParser +} diff --git a/Sources/ConfidentialCore/Parsing/Parsers/ParsingError.swift b/Sources/ConfidentialCore/Parsing/Parsers/ParsingError.swift new file mode 100644 index 0000000..ee1f742 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/Parsers/ParsingError.swift @@ -0,0 +1,13 @@ +enum ParsingError: Error { + case assertionFailed(description: String) +} + +extension ParsingError: CustomStringConvertible { + + var description: String { + switch self { + case let .assertionFailed(description): + return description + } + } +} diff --git a/Sources/ConfidentialCore/Parsing/ParsersConvenienceInitializers.swift b/Sources/ConfidentialCore/Parsing/ParsersConvenienceInitializers.swift new file mode 100644 index 0000000..3b94747 --- /dev/null +++ b/Sources/ConfidentialCore/Parsing/ParsersConvenienceInitializers.swift @@ -0,0 +1,46 @@ +import Parsing +import SwiftSyntaxBuilder + +public typealias AnyAlgorithmParser = AnyParser +public typealias AnySecretsParser = AnyParser + +public extension Parsers.ModelTransform.SourceSpecification +where +AlgorithmParser == AnyAlgorithmParser, +SecretsParser == AnySecretsParser { + + private typealias OneOfManyTechniques = Parsers.OneOfMany> + + init() { + self.init( + algorithmParser: ConfidentialCore.AlgorithmParser( + obfuscationStepParser: ObfuscationStepParser { + CompressionTechniqueParser().eraseToAnyParser() + EncryptionTechniqueParser().eraseToAnyParser() + RandomizationTechniqueParser().eraseToAnyParser() + } + ).eraseToAnyParser(), + secretsParser: ConfidentialCore.SecretsParser( + namespaceParser: SecretNamespaceParser() + ).eraseToAnyParser() + ) + } +} + +public typealias AnyCodeBlockParser = AnyParser + +public extension Parsers.CodeGeneration.SourceFile +where +CodeBlockParsers == AnyCodeBlockParser { + + init() { + self.init { + NamespaceDeclParser( + membersParser: NamespaceMembersParser( + secretDeclParser: SecretDeclParser() + ), + deobfuscateDataFunctionDeclParser: DeobfuscateDataFunctionDeclParser(functionNestingLevel: 1) + ).eraseToAnyParser() + } + } +} diff --git a/Sources/ConfidentialCore/SyntaxBuilders/Declarations/DeobfuscateDataFunctionDecl.swift b/Sources/ConfidentialCore/SyntaxBuilders/Declarations/DeobfuscateDataFunctionDecl.swift new file mode 100644 index 0000000..99b316e --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxBuilders/Declarations/DeobfuscateDataFunctionDecl.swift @@ -0,0 +1,59 @@ +import Foundation +import SwiftSyntax +import SwiftSyntaxBuilder + +struct DeobfuscateDataFunctionDecl: DeclBuildable { + + private let declNestingLevel: UInt8 + private let body: ExpressibleAsSyntaxBuildable + + init(declNestingLevel: UInt8 = .zero, body: ExpressibleAsSyntaxBuildable) { + self.declNestingLevel = declNestingLevel + self.body = body + } + + func buildDecl(format: Format, leadingTrivia: Trivia?) -> DeclSyntax { + makeUnderlyingDecl().buildDecl(format: format, leadingTrivia: leadingTrivia) + } +} + +private extension DeobfuscateDataFunctionDecl { + + func makeUnderlyingDecl() -> DeclBuildable { + let dataTypeInfo = TypeInfo(of: Data.self) + let indentation = Int(declNestingLevel) * C.Code.Format.indentWidth + return FunctionDecl( + identifier: .identifier(C.Code.Generation.deobfuscateDataFuncName), + signature: FunctionSignature( + input: ParameterClause { + FunctionParameter( + attributes: .none, + firstName: .wildcard, + secondName: .identifier(C.Code.Generation.deobfuscateDataFuncParamName), + colon: .colon, + type: SimpleTypeIdentifier(dataTypeInfo.fullyQualifiedName) + ) + }, + throwsOrRethrowsKeyword: .throws.withLeadingTrivia(.spaces(1)), + output: ReturnClause( + returnType: SimpleTypeIdentifier(dataTypeInfo.fullyQualifiedName) + ) + ), + body: CodeBlock( + leftBrace: .leftBrace.withLeadingTrivia(.spaces(1)), + rightBrace: .rightBrace.withLeadingTrivia(.spaces(indentation)) + ) { + CodeBlockItem(item: body) + }, + attributesBuilder: { + InlineAlwaysAttribute( + leadingTrivia: .newlines(1).appending(.spaces(indentation)) + ) + }, + modifiersBuilder: { + DeclModifier(name: .private(leadingNewlines: 1, followedByLeadingSpaces: indentation)) + DeclModifier(name: .static) + } + ) + } +} diff --git a/Sources/ConfidentialCore/SyntaxBuilders/Declarations/SecretDecl.swift b/Sources/ConfidentialCore/SyntaxBuilders/Declarations/SecretDecl.swift new file mode 100644 index 0000000..b72e8a6 --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxBuilders/Declarations/SecretDecl.swift @@ -0,0 +1,67 @@ +import ConfidentialKit +import SwiftSyntax +import SwiftSyntaxBuilder + +struct SecretDecl: DeclBuildable { + + private let name: TokenSyntax + private let dataArgumentExpression: ExpressibleAsArrayExpr + private let dataAccessWrapper: ExpressibleAsCustomAttribute + + init( + name: String, + dataArgumentExpression: ExpressibleAsArrayExpr, + dataAccessWrapper: ExpressibleAsCustomAttribute + ) { + self.name = .identifier(name) + self.dataArgumentExpression = dataArgumentExpression + self.dataAccessWrapper = dataAccessWrapper + } + + func buildDecl(format: Format, leadingTrivia: Trivia?) -> DeclSyntax { + makeUnderlyingDecl().buildDecl(format: format, leadingTrivia: leadingTrivia) + } +} + +private extension SecretDecl { + + static let dataArgumentName: String = "data" + + func makeUnderlyingDecl() -> DeclBuildable { + VariableDecl( + letOrVarKeyword: .var, + attributesBuilder: { + dataAccessWrapper.createCustomAttribute() + }, + modifiersBuilder: { + DeclModifier(name: .static(leadingNewlines: 1)) + }, + bindingsBuilder: { + PatternBinding( + pattern: IdentifierPattern(identifier: name), + typeAnnotation: TypeAnnotation( + TypeInfo(of: Obfuscation.Secret.self).fullyQualifiedName + ), + initializer: InitializerClause( + value: InitializerCallExpr { + TupleExprElement( + label: .identifier(Self.dataArgumentName), + colon: .colon, + expression: dataArgumentExpression.createArrayExpr() + ) + } + ) + ) + } + ) + } +} + +protocol ExpressibleAsSecretDecl { + func createSecretDecl() -> SecretDecl +} + +extension SecretDecl: ExpressibleAsSecretDecl { + + func createSecretDecl() -> SecretDecl { self } +} diff --git a/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataCompressorInitializerCallExpr.swift b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataCompressorInitializerCallExpr.swift new file mode 100644 index 0000000..a7f4dcd --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataCompressorInitializerCallExpr.swift @@ -0,0 +1,43 @@ +import ConfidentialKit +import SwiftSyntax +import SwiftSyntaxBuilder + +struct DataCompressorInitializerCallExpr: ExprBuildable { + + typealias Algorithm = Obfuscation.Compression.CompressionAlgorithm + + private let compressionAlgorithm: Algorithm + + init(compressionAlgorithm: Algorithm) { + self.compressionAlgorithm = compressionAlgorithm + } + + func buildExpr(format: Format, leadingTrivia: Trivia?) -> ExprSyntax { + makeUnderlyingExpr().buildExpr(format: format, leadingTrivia: leadingTrivia) + } +} + +private extension DataCompressorInitializerCallExpr { + + static let algorithmArgumentName: String = "algorithm" + + func makeUnderlyingExpr() -> ExprBuildable { + FunctionCallExpr( + IdentifierExpr( + TypeInfo(of: Obfuscation.Compression.DataCompressor.self).fullyQualifiedName + ), + leftParen: .leftParen, + rightParen: .rightParen, + argumentListBuilder: { + TupleExprElement( + label: .identifier(Self.algorithmArgumentName), + colon: .colon, + expression: MemberAccessExpr( + dot: .prefixPeriod, + name: .identifier(compressionAlgorithm.name) + ) + ) + } + ) + } +} diff --git a/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataCrypterInitializerCallExpr.swift b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataCrypterInitializerCallExpr.swift new file mode 100644 index 0000000..096323e --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataCrypterInitializerCallExpr.swift @@ -0,0 +1,43 @@ +import ConfidentialKit +import SwiftSyntax +import SwiftSyntaxBuilder + +struct DataCrypterInitializerCallExpr: ExprBuildable { + + typealias Algorithm = Obfuscation.Encryption.SymmetricEncryptionAlgorithm + + private let encryptionAlgorithm: Algorithm + + init(encryptionAlgorithm: Algorithm) { + self.encryptionAlgorithm = encryptionAlgorithm + } + + func buildExpr(format: Format, leadingTrivia: Trivia?) -> ExprSyntax { + makeUnderlyingExpr().buildExpr(format: format, leadingTrivia: leadingTrivia) + } +} + +private extension DataCrypterInitializerCallExpr { + + static let algorithmArgumentName: String = "algorithm" + + func makeUnderlyingExpr() -> ExprBuildable { + FunctionCallExpr( + IdentifierExpr( + TypeInfo(of: Obfuscation.Encryption.DataCrypter.self).fullyQualifiedName + ), + leftParen: .leftParen, + rightParen: .rightParen, + argumentListBuilder: { + TupleExprElement( + label: .identifier(Self.algorithmArgumentName), + colon: .colon, + expression: MemberAccessExpr( + dot: .prefixPeriod, + name: .identifier(encryptionAlgorithm.name) + ) + ) + } + ) + } +} diff --git a/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataShufflerInitializerCallExpr.swift b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataShufflerInitializerCallExpr.swift new file mode 100644 index 0000000..beb683d --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DataShufflerInitializerCallExpr.swift @@ -0,0 +1,38 @@ +import ConfidentialKit +import SwiftSyntax +import SwiftSyntaxBuilder + +struct DataShufflerInitializerCallExpr: ExprBuildable { + + private let nonce: UInt64 + + init(nonce: UInt64) { + self.nonce = nonce + } + + func buildExpr(format: Format, leadingTrivia: Trivia?) -> ExprSyntax { + makeUnderlyingExpr().buildExpr(format: format, leadingTrivia: leadingTrivia) + } +} + +private extension DataShufflerInitializerCallExpr { + + static let nonceArgumentName: String = "nonce" + + func makeUnderlyingExpr() -> ExprBuildable { + FunctionCallExpr( + IdentifierExpr( + TypeInfo(of: Obfuscation.Randomization.DataShuffler.self).fullyQualifiedName + ), + leftParen: .leftParen, + rightParen: .rightParen, + argumentListBuilder: { + TupleExprElement( + label: .identifier(Self.nonceArgumentName), + colon: .colon, + expression: IntegerLiteralExpr(digits: "\(nonce)") + ) + } + ) + } +} diff --git a/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DeobfuscateFunctionAccessExpr.swift b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DeobfuscateFunctionAccessExpr.swift new file mode 100644 index 0000000..11d8495 --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/DeobfuscateFunctionAccessExpr.swift @@ -0,0 +1,34 @@ +import ConfidentialKit +import SwiftSyntax +import SwiftSyntaxBuilder + +struct DeobfuscateFunctionAccessExpr: ExprBuildable { + + private let deobfuscationStepInitializerExpr: ExpressibleAsExprBuildable + private let dotIndentWidth: Int + + init(_ deobfuscationStepInitializerExpr: ExpressibleAsExprBuildable, dotIndentWidth: Int) { + self.deobfuscationStepInitializerExpr = deobfuscationStepInitializerExpr + self.dotIndentWidth = dotIndentWidth + } + + func buildExpr(format: Format, leadingTrivia: Trivia?) -> ExprSyntax { + makeUnderlyingExpr().buildExpr(format: format, leadingTrivia: leadingTrivia) + } +} + +private extension DeobfuscateFunctionAccessExpr { + + static let deobfuscateFuncName: String = "deobfuscate" + + func makeUnderlyingExpr() -> ExprBuildable { + MemberAccessExpr( + base: deobfuscationStepInitializerExpr, + dot: .period( + leadingNewlines: 1, + followedByLeadingSpaces: dotIndentWidth + ), + name: .identifier(Self.deobfuscateFuncName) + ) + } +} diff --git a/Sources/ConfidentialCore/SyntaxBuilders/Expressions/InitializerCallExpr.swift b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/InitializerCallExpr.swift new file mode 100644 index 0000000..b74882b --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxBuilders/Expressions/InitializerCallExpr.swift @@ -0,0 +1,31 @@ +import SwiftSyntax +import SwiftSyntaxBuilder + +struct InitializerCallExpr: ExprBuildable { + + private let argumentList: ExpressibleAsTupleExprElementList + + init( + @TupleExprElementListBuilder argumentListBuilder: () -> ExpressibleAsTupleExprElementList = { + TupleExprElementList.empty + } + ) { + self.argumentList = argumentListBuilder() + } + + func buildExpr(format: Format, leadingTrivia: Trivia?) -> ExprSyntax { + makeUnderlyingExpr().buildExpr(format: format, leadingTrivia: leadingTrivia) + } +} + +private extension InitializerCallExpr { + + func makeUnderlyingExpr() -> ExprBuildable { + FunctionCallExpr( + calledExpression: MemberAccessExpr(dot: .period, name: .`init`.withoutTrivia()), + leftParen: .leftParen, + argumentList: argumentList, + rightParen: .rightParen + ) + } +} diff --git a/Sources/ConfidentialCore/SyntaxBuilders/InlineAlwaysAttribute.swift b/Sources/ConfidentialCore/SyntaxBuilders/InlineAlwaysAttribute.swift new file mode 100644 index 0000000..2e0c279 --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxBuilders/InlineAlwaysAttribute.swift @@ -0,0 +1,29 @@ +import SwiftSyntax +import SwiftSyntaxBuilder + +struct InlineAlwaysAttribute: SyntaxBuildable { + + private let leadingTrivia: Trivia? + + init(leadingTrivia: Trivia? = .none) { + self.leadingTrivia = leadingTrivia + } + + func buildSyntax(format: Format, leadingTrivia: Trivia?) -> Syntax { + makeUnderlyingSyntax().buildSyntax(format: format, leadingTrivia: leadingTrivia) + } +} + +private extension InlineAlwaysAttribute { + + func makeUnderlyingSyntax() -> SyntaxBuildable { + Attribute( + atSignToken: .atSign.withLeadingTrivia(leadingTrivia ?? .zero), + attributeName: .identifier("inline"), + leftParen: .leftParen, + argument: IdentifierExpr("__always"), + rightParen: .rightParen, + tokenList: .none + ) + } +} diff --git a/Sources/ConfidentialCore/SyntaxText/SourceFileText.swift b/Sources/ConfidentialCore/SyntaxText/SourceFileText.swift new file mode 100644 index 0000000..eae8782 --- /dev/null +++ b/Sources/ConfidentialCore/SyntaxText/SourceFileText.swift @@ -0,0 +1,28 @@ +import Foundation +import SwiftSyntax +import SwiftSyntaxBuilder + +public struct SourceFileText: Equatable { + + private let syntax: Syntax + + init(from sourceFile: ExpressibleAsSourceFile) { + self.syntax = sourceFile + .createSourceFile() + .buildSyntax(format: .init(indentWidth: .zero)) + } + + public func write(to url: URL, encoding: String.Encoding = .utf8) throws { + var text = "" + syntax.write(to: &text) + + try text + .trimmingCharacters(in: .newlines) + .write(to: url, atomically: true, encoding: encoding) + } +} + +extension SourceFileText: CustomStringConvertible { + + public var description: String { syntax.description } +} diff --git a/Sources/ConfidentialCore/Utils/TypeInfo.swift b/Sources/ConfidentialCore/Utils/TypeInfo.swift new file mode 100644 index 0000000..da72fca --- /dev/null +++ b/Sources/ConfidentialCore/Utils/TypeInfo.swift @@ -0,0 +1,40 @@ +struct TypeInfo { + + private let type: Any.Type + + init(of type: Any.Type) { + self.type = type + } +} + +extension TypeInfo { + + var fullyQualifiedName: String { + let name = String(reflecting: type) + guard let typeLocationEndIndex = name.lastIndex(of: ":") else { + return name + } + + return .init(name.suffix(from: typeLocationEndIndex).dropFirst()) + } + + var moduleName: String { + .init(fullyQualifiedName.prefix { $0 != "." }) + } + + var fullName: String { + let fullyQualifiedName = fullyQualifiedName + guard + let index = fullyQualifiedName.firstIndex(of: "."), + fullyQualifiedName.distance(from: index, to: fullyQualifiedName.endIndex) > 1 + else { + fatalError("Unexpected metatype string representation") + } + + return .init(fullyQualifiedName.suffix(from: index).dropFirst()) + } + + var name: String { + .init(describing: type) + } +} diff --git a/Sources/ConfidentialKit/Coding/DataDecoder.swift b/Sources/ConfidentialKit/Coding/DataDecoder.swift new file mode 100644 index 0000000..8026c62 --- /dev/null +++ b/Sources/ConfidentialKit/Coding/DataDecoder.swift @@ -0,0 +1,8 @@ +import Foundation + +@usableFromInline +protocol DataDecoder { + func decode(_ type: D.Type, from data: Data) throws -> D +} + +extension JSONDecoder: DataDecoder {} diff --git a/Sources/ConfidentialKit/Extensions/CryptoKit/SymmetricKeySize/SymmetricKeySize+ByteCount.swift b/Sources/ConfidentialKit/Extensions/CryptoKit/SymmetricKeySize/SymmetricKeySize+ByteCount.swift new file mode 100644 index 0000000..c0e8218 --- /dev/null +++ b/Sources/ConfidentialKit/Extensions/CryptoKit/SymmetricKeySize/SymmetricKeySize+ByteCount.swift @@ -0,0 +1,13 @@ +import CryptoKit + +extension SymmetricKeySize { + + /// The number of bytes in the key. + /// + /// The returned value is not rounded up, since ``init(bitCount:)`` only accepts + /// positive integers that are a multiple of 8. + @usableFromInline + var byteCount: Int { + bitCount / 8 + } +} diff --git a/Sources/ConfidentialKit/Extensions/Swift/BinaryInteger/BinaryInteger+Bytes.swift b/Sources/ConfidentialKit/Extensions/Swift/BinaryInteger/BinaryInteger+Bytes.swift new file mode 100644 index 0000000..73b83bd --- /dev/null +++ b/Sources/ConfidentialKit/Extensions/Swift/BinaryInteger/BinaryInteger+Bytes.swift @@ -0,0 +1,23 @@ +public extension BinaryInteger { + + /// This value's binary representation, as a sequence of contiguous bytes. + @inlinable + @inline(__always) + var bytes: [UInt8] { withUnsafeBytes(of: self, [UInt8].init) } + + /// Creates a new instance from the given binary representation. + /// + /// - Parameter bytes: A collection containing the bytes of this valueโ€™s binary representation, + /// in order from the least significant to most significant. + @inlinable + @inline(__always) + init(bytes: Bytes) where Bytes.Element == UInt8 { + var bytes = Array(bytes) + let size = MemoryLayout.stride + if bytes.count < size { + bytes.append(contentsOf: (bytes.count.. Data +} diff --git a/Sources/ConfidentialKit/Obfuscation/Obfuscation+Secret.swift b/Sources/ConfidentialKit/Obfuscation/Obfuscation+Secret.swift new file mode 100644 index 0000000..39a260e --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Obfuscation+Secret.swift @@ -0,0 +1,17 @@ +public extension Obfuscation { + + /// A model representing an obfuscated secret. + struct Secret: Equatable { + + /// The obfuscated secret's bytes. + public let data: [UInt8] + + /// Creates a new instance of a sequence containing obfuscated secret's bytes. + /// + /// - Parameter data: The sequence of obfuscated secret's bytes. + @inlinable + public init(data: Data) where Data.Element == UInt8 { + self.data = .init(data) + } + } +} diff --git a/Sources/ConfidentialKit/Obfuscation/Obfuscation+SupportedDataTypes.swift b/Sources/ConfidentialKit/Obfuscation/Obfuscation+SupportedDataTypes.swift new file mode 100644 index 0000000..7f0d7ed --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Obfuscation+SupportedDataTypes.swift @@ -0,0 +1,11 @@ +public extension Obfuscation { + + /// A namespace for the plain data types supported by ``Obfuscation`` API. + enum SupportedDataTypes { + /// A type that represents a sequence of values. + public typealias Array = [String] + + /// A type that represents a single value. + public typealias SingleValue = String + } +} diff --git a/Sources/ConfidentialKit/Obfuscation/Obfuscation.swift b/Sources/ConfidentialKit/Obfuscation/Obfuscation.swift new file mode 100644 index 0000000..87ca0c5 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Obfuscation.swift @@ -0,0 +1,19 @@ +/// A namespace for types associated with obfuscation-related tasks, which all together +/// constitute an API for (de)obfuscating secret data. +/// +/// The various implementations of obfuscation techniques are defined within their own +/// namespaces defined as extensions on ``Obfuscation``. +/// +/// Your choice of technique determines the strategy used for data obfuscation: +/// - The ``Obfuscation/Compression`` namespace encapsulates +/// implementations of technique involving data compression/decompression. +/// - The ``Obfuscation/Encryption`` namespace encapsulates +/// implementations of technique involving data encryption/decryption. +/// - The ``Obfuscation/Randomization`` namespace encapsulates +/// implementations of technique involving data randomization. +/// +/// > Important: The ``ConfidentialKit`` library was designed to be used in +/// conjunction with `confidential` CLI tool and, as such, it only ships +/// with a subset of API needed for deobfuscating obfuscated secret data +/// embedded in the application code. +public enum Obfuscation {} diff --git a/Sources/ConfidentialKit/Obfuscation/PropertyWrappers/Obfuscated.swift b/Sources/ConfidentialKit/Obfuscation/PropertyWrappers/Obfuscated.swift new file mode 100644 index 0000000..09479c9 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/PropertyWrappers/Obfuscated.swift @@ -0,0 +1,65 @@ +import Foundation + +/// A property wrapper that can deobfuscate the wrapped secret value. +@propertyWrapper +public struct Obfuscated { + + /// A type that represents a wrapped value. + public typealias Value = Obfuscation.Secret + + /// A type that represents a deobfuscation function. + public typealias DeobfuscateDataFunc = (Data) throws -> Data + + @usableFromInline + let deobfuscateData: DeobfuscateDataFunc + + @usableFromInline + let decoder: DataDecoder + + /// The underlying secret value. + public let wrappedValue: Value + + /// A plain secret value after transforming obfuscated secret's data with a deobfuscation function. + @inlinable + public var projectedValue: PlainValue { + let secretData = Data(wrappedValue.data) + let value: PlainValue + + do { + let deobfuscatedData = try deobfuscateData(secretData) + value = try decoder.decode(PlainValue.self, from: deobfuscatedData) + } catch { + preconditionFailure("Unexpected error: \(error)") + } + + return value + } + + @usableFromInline + init( + wrappedValue: Value, + deobfuscateData: @escaping DeobfuscateDataFunc, + decoder: DataDecoder + ) { + self.wrappedValue = wrappedValue + self.deobfuscateData = deobfuscateData + self.decoder = decoder + } + + /// Creates a property that can deobfuscate the wrapped secret value using the given closure. + /// + /// - Parameters: + /// - wrappedValue: A secret value containing obfuscated data. + /// - deobfuscateData: The closure to execute when calling ``projectedValue``. + @inlinable + public init( + wrappedValue: Value, + _ deobfuscateData: @escaping DeobfuscateDataFunc + ) { + self.init( + wrappedValue: wrappedValue, + deobfuscateData: deobfuscateData, + decoder: JSONDecoder() + ) + } +} diff --git a/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/CompressionAlgorithm.swift b/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/CompressionAlgorithm.swift new file mode 100644 index 0000000..93cfa10 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/CompressionAlgorithm.swift @@ -0,0 +1,7 @@ +import Foundation + +public extension Obfuscation.Compression { + + /// An algorithm that indicates how to compress or decompress data. + typealias CompressionAlgorithm = NSData.CompressionAlgorithm +} diff --git a/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/DataCompressor.swift b/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/DataCompressor.swift new file mode 100644 index 0000000..e402dd3 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/DataCompressor.swift @@ -0,0 +1,33 @@ +import Foundation + +public extension Obfuscation.Compression { + + /// An implementation of obfuscation technique utilizing data compression. + /// + /// See ``CompressionAlgorithm`` for a list of supported compression algorithms. + struct DataCompressor: DataDeobfuscationStep { + + /// An algorithm used to compress and decompress the data. + public let algorithm: CompressionAlgorithm + + /// Creates a new instance with the specified compression algorithm. + /// + /// - Parameter algorithm: An algorithm used to compress and decompress the data. + @inlinable + @inline(__always) + public init(algorithm: CompressionAlgorithm) { + self.algorithm = algorithm + } + + /// Decompresses the given data using preset ``algorithm``. + /// + /// - Parameter data: A compressed input data. + /// - Returns: A decompressed output data. + @inlinable + @inline(__always) + public func deobfuscate(_ data: Data) throws -> Data { + let decompressedData = try NSData(data: data).decompressed(using: algorithm) + return .init(referencing: decompressedData) + } + } +} diff --git a/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/Obfuscation+Compression.swift b/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/Obfuscation+Compression.swift new file mode 100644 index 0000000..f2d6e14 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Techniques/Compression/Obfuscation+Compression.swift @@ -0,0 +1,5 @@ +public extension Obfuscation { + + /// A namespace for types that use data compression as obfuscation technique. + enum Compression {} +} diff --git a/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/DataCrypter.swift b/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/DataCrypter.swift new file mode 100644 index 0000000..d87c572 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/DataCrypter.swift @@ -0,0 +1,47 @@ +import CryptoKit +import Foundation + +public extension Obfuscation.Encryption { + + /// An implementation of obfuscation technique utilizing data encryption. + /// + /// See ``SymmetricEncryptionAlgorithm`` for a list of supported encryption algorithms. + struct DataCrypter: DataDeobfuscationStep { + + /// An algorithm used to encrypt and decrypt the data. + public let algorithm: SymmetricEncryptionAlgorithm + + /// Creates a new instance with the specified symmetric encryption algorithm. + /// + /// - Parameter algorithm: An algorithm used to encrypt and decrypt the data. + @inlinable + @inline(__always) + public init(algorithm: SymmetricEncryptionAlgorithm) { + self.algorithm = algorithm + } + + /// Decrypts the given data using preset ``algorithm``. + /// + /// - Parameter data: An encrypted input data. + /// - Returns: A decrypted output data. + @inlinable + @inline(__always) + public func deobfuscate(_ data: Data) throws -> Data { + var obfuscatedData = data + let keyData = obfuscatedData.suffix(algorithm.keySize.byteCount) + obfuscatedData.removeLast(algorithm.keySize.byteCount) + + let deobfuscatedData: Data + switch algorithm { + case .aes128GCM, .aes192GCM, .aes256GCM: + let sealedBox = try AES.GCM.SealedBox(combined: obfuscatedData) + deobfuscatedData = try AES.GCM.open(sealedBox, using: .init(data: keyData)) + case .chaChaPoly: + let sealedBox = try ChaChaPoly.SealedBox(combined: obfuscatedData) + deobfuscatedData = try ChaChaPoly.open(sealedBox, using: .init(data: keyData)) + } + + return deobfuscatedData + } + } +} diff --git a/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/Obfuscation+Encryption.swift b/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/Obfuscation+Encryption.swift new file mode 100644 index 0000000..09155f8 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/Obfuscation+Encryption.swift @@ -0,0 +1,5 @@ +public extension Obfuscation { + + /// A namespace for types that use data encryption as obfuscation technique. + enum Encryption {} +} diff --git a/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/SymmetricEncryptionAlgorithm.swift b/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/SymmetricEncryptionAlgorithm.swift new file mode 100644 index 0000000..1e88571 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Techniques/Encryption/SymmetricEncryptionAlgorithm.swift @@ -0,0 +1,39 @@ +import CryptoKit + +public extension Obfuscation.Encryption { + + /// A symmetric algorithm that indicates how to encrypt or decrypt data. + enum SymmetricEncryptionAlgorithm: String { + /// The Advanced Encryption Standard (AES) algorithm in Galois/Counter Mode (GCM) + /// with 128-bit key. + case aes128GCM + + /// The Advanced Encryption Standard (AES) algorithm in Galois/Counter Mode (GCM) + /// with 192-bit key. + case aes192GCM + + /// The Advanced Encryption Standard (AES) algorithm in Galois/Counter Mode (GCM) + /// with 256-bit key. + case aes256GCM + + /// The ChaCha20-Poly1305 algorithm. + case chaChaPoly + } +} + +public extension Obfuscation.Encryption.SymmetricEncryptionAlgorithm { + + /// The size of the symmetric cryptographic key associated with the algorithm. + var keySize: SymmetricKeySize { + switch self { + case .aes128GCM: + return .bits128 + case .aes192GCM: + return .bits192 + case .aes256GCM: + return .bits256 + case .chaChaPoly: + return .bits256 + } + } +} diff --git a/Sources/ConfidentialKit/Obfuscation/Techniques/Randomization/DataShuffler.swift b/Sources/ConfidentialKit/Obfuscation/Techniques/Randomization/DataShuffler.swift new file mode 100644 index 0000000..093eecd --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Techniques/Randomization/DataShuffler.swift @@ -0,0 +1,110 @@ +import Foundation + +public extension Obfuscation.Randomization { + + /// An implementation of obfuscation technique utilizing data randomization. + /// + /// The ``DataShuffler`` uses a pseudorandom number generator (PRNG) to + /// shuffle the bytes stored in ``Data`` instance being processed, along with a + /// ``nonce``, which is used to obfuscate the shuffling parameters. + /// + /// > Warning: The current implementation of this technique is best suited for secrets of + /// which size does not exceed 256 bytes. For larger secrets, the size of the + /// obfuscated data will grow from 2N to 3N, where N is the input data size + /// in bytes, or even 5N (32-bit platform) or 9N (64-bit platform) if the size of + /// input data is larger than 65 536 bytes. + struct DataShuffler: DataDeobfuscationStep { + + /// The nonce used to obfuscate the data. + public let nonce: UInt64 + + /// Creates a new instance with the specified cryptographic nonce. + /// + /// - Parameter nonce: The nonce. + @inlinable + @inline(__always) + public init(nonce: UInt64) { + self.nonce = nonce + } + + /// Deshuffles the given data using preset ``nonce``. + /// + /// - Parameter data: A shuffled input data. + /// - Returns: A deshuffled output data. + @inlinable + @inline(__always) + public func deobfuscate(_ data: Data) throws -> Data { + let countByteWidth = Int.byteWidth + let nonceBytes = nonce.bytes + let count = data + .prefix(upTo: countByteWidth) + .withUnsafeBytes { $0.load(as: Int.self) } ^ .init(bytes: nonceBytes) + let indexByteWidthPos = countByteWidth + count + let indexByteWidth = data[indexByteWidthPos] + let indexes = Internal.deobfuscateIndexes( + bytes: .init(data.suffix(from: indexByteWidthPos + 1)), + byteWidth: indexByteWidth, + nonceBytes: nonceBytes + ) + let shuffledBytes = data.subdata(in: countByteWidth.. [UInt8] { + var result: [UInt8] = .init(repeating: .zero, count: bytes.count) + indexes.enumerated().forEach { newIdx, oldIdx in + result[newIdx] = bytes[oldIdx] + } + + return result + } + + @usableFromInline + @inline(__always) + static func deobfuscateIndexes(bytes: [UInt8], byteWidth: UInt8, nonceBytes: [UInt8]) -> [Int] { + var bytes = bytes[...] + let byteWidth = Int(byteWidth) + switch byteWidth { + case UInt8.byteWidth: + return deobfuscateIndexes(bytes: &bytes, indexType: UInt8.self, nonceBytes: nonceBytes) + case UInt16.byteWidth: + return deobfuscateIndexes(bytes: &bytes, indexType: UInt16.self, nonceBytes: nonceBytes) + default: + return deobfuscateIndexes(bytes: &bytes, indexType: Int.self, nonceBytes: nonceBytes) + } + } + + @usableFromInline + @inline(__always) + static func deobfuscateIndexes( + bytes: inout ArraySlice, + indexType: I.Type, + nonceBytes: [UInt8] + ) -> [Int] { + let byteWidth = indexType.byteWidth + var indexes: [Int] = [] + while !bytes.isEmpty { + let index = Int( + bytes + .prefix(upTo: bytes.startIndex + byteWidth) + .withUnsafeBytes { $0.load(as: indexType) } ^ .init(bytes: nonceBytes) + ) + indexes.append(index) + bytes.removeFirst(byteWidth) + } + + return indexes + } + } +} diff --git a/Sources/ConfidentialKit/Obfuscation/Techniques/Randomization/Obfuscation+Randomization.swift b/Sources/ConfidentialKit/Obfuscation/Techniques/Randomization/Obfuscation+Randomization.swift new file mode 100644 index 0000000..290bea2 --- /dev/null +++ b/Sources/ConfidentialKit/Obfuscation/Techniques/Randomization/Obfuscation+Randomization.swift @@ -0,0 +1,5 @@ +public extension Obfuscation { + + /// A namespace for types that use data randomization as obfuscation technique. + enum Randomization {} +} diff --git a/Sources/confidential/Confidential.swift b/Sources/confidential/Confidential.swift new file mode 100644 index 0000000..cc9bc59 --- /dev/null +++ b/Sources/confidential/Confidential.swift @@ -0,0 +1,14 @@ +import ArgumentParser + +@main +struct Confidential: ParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "confidential", + abstract: "A command-line tool to obfuscate secret literals embedded in Swift project.", + subcommands: [ + Obfuscate.self + ], + defaultSubcommand: Obfuscate.self + ) +} diff --git a/Sources/confidential/Errors/RuntimeError.swift b/Sources/confidential/Errors/RuntimeError.swift new file mode 100644 index 0000000..cc97866 --- /dev/null +++ b/Sources/confidential/Errors/RuntimeError.swift @@ -0,0 +1,3 @@ +struct RuntimeError: Error, CustomStringConvertible { + let description: String +} diff --git a/Sources/confidential/Subcommands/Obfuscate.swift b/Sources/confidential/Subcommands/Obfuscate.swift new file mode 100644 index 0000000..9c1f1ed --- /dev/null +++ b/Sources/confidential/Subcommands/Obfuscate.swift @@ -0,0 +1,58 @@ +import ArgumentParser +import ConfidentialCore +import Foundation +import Yams + +extension Confidential { + + struct Obfuscate: ParsableCommand { + + static let configuration = CommandConfiguration( + commandName: "obfuscate", + abstract: "Obfuscate secret literals.", + discussion: """ + The generated Swift code provides accessors for each secret literal, \ + grouped into namespaces as defined in configuration file. \ + The accessor allows for retrieving a deobfuscated literal at \ + runtime. + """ + ) + + @Option( + help: "The path to a Confidential configuration file.", + transform: URL.init(fileURLWithPath:) + ) + var configuration: URL + + @Option( + help: "The path to an output source file where the generated Swift code is to be written.", + transform: URL.init(fileURLWithPath:) + ) + var output: URL + + private var fileManager: FileManager { FileManager.default } + + mutating func run() throws { + guard fileManager.isReadableFile(atPath: configuration.path) else { + throw RuntimeError(description: #"Unable to read configuration file at \#(configuration.path)"#) + } + + let configurationYAML = try Data(contentsOf: configuration) + let configuration = try YAMLDecoder().decode(Configuration.self, from: configurationYAML) + + guard fileManager.createFile(atPath: output.path, contents: .none) else { + throw RuntimeError(description: #"Failed to create output file at "\#(output.path)""#) + } + + var sourceSpecification = try Parsers.ModelTransform.SourceSpecification() + .parse(configuration) + + try SourceObfuscator().obfuscate(&sourceSpecification) + + let sourceFileText = try Parsers.CodeGeneration.SourceFile() + .parse(&sourceSpecification) + + try sourceFileText.write(to: output) + } + } +} diff --git a/Tests/ConfidentialCoreTests/Extensions/Foundation/Data/Data+HexStringTests.swift b/Tests/ConfidentialCoreTests/Extensions/Foundation/Data/Data+HexStringTests.swift new file mode 100644 index 0000000..5f80345 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Extensions/Foundation/Data/Data+HexStringTests.swift @@ -0,0 +1,74 @@ +@testable import ConfidentialCore +import XCTest + +final class Data_HexStringTests: XCTestCase { + + private let bytesStub: [UInt8] = [0xff, 0x36, 0xb4, 0xcb, 0x34, 0xff, 0x8c, 0x8f, 0xbf, 0x0f, 0x43, 0x45] + + func test_givenEmptyData_whenHexEncodedStringComponents_thenReturnsEmptyArray() { + // given + let data = Data() + + // when + let hexComponents = data.hexEncodedStringComponents() + + // then + XCTAssertTrue(hexComponents.isEmpty) + } + + func test_givenNonEmptyData_whenHexEncodedStringComponents_thenReturnsExpectedComponents() { + // given + let data = Data(bytesStub) + + // when + let hexComponents = data.hexEncodedStringComponents() + + // then + XCTAssertEqual( + ["ff", "36", "b4", "cb", "34", "ff", "8c", "8f", "bf", "0f", "43", "45"], + hexComponents + ) + } + + func test_givenNonEmptyData_whenHexEncodedStringComponentsOptionsUpperCase_thenReturnsExpectedComponents() { + // given + let data = Data(bytesStub) + + // when + let hexComponents = data.hexEncodedStringComponents(options: .upperCase) + + // then + XCTAssertEqual( + ["FF", "36", "B4", "CB", "34", "FF", "8C", "8F", "BF", "0F", "43", "45"], + hexComponents + ) + } + + func test_givenNonEmptyData_whenHexEncodedStringComponentsOptionsNumericLiteral_thenReturnsExpectedComponents() { + // given + let data = Data(bytesStub) + + // when + let hexComponents = data.hexEncodedStringComponents(options: .numericLiteral) + + // then + XCTAssertEqual( + ["0xff", "0x36", "0xb4", "0xcb", "0x34", "0xff", "0x8c", "0x8f", "0xbf", "0x0f", "0x43", "0x45"], + hexComponents + ) + } + + func test_givenNonEmptyData_whenHexEncodedStringComponentsOptionsUpperCaseAndNumericLiteral_thenReturnsExpectedComponents() { + // given + let data = Data(bytesStub) + + // when + let hexComponents = data.hexEncodedStringComponents(options: [.upperCase, .numericLiteral]) + + // then + XCTAssertEqual( + ["0xFF", "0x36", "0xB4", "0xCB", "0x34", "0xFF", "0x8C", "0x8F", "0xBF", "0x0F", "0x43", "0x45"], + hexComponents + ) + } +} diff --git a/Tests/ConfidentialCoreTests/Extensions/Swift/Encodable/Encodable+TypeErasureTests.swift b/Tests/ConfidentialCoreTests/Extensions/Swift/Encodable/Encodable+TypeErasureTests.swift new file mode 100644 index 0000000..3d66533 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Extensions/Swift/Encodable/Encodable+TypeErasureTests.swift @@ -0,0 +1,21 @@ +@testable import ConfidentialCore +import XCTest + +final class Encodable_TypeErasureTests: XCTestCase { + + func test_givenTypeErasedEncodable_whenJSONEncoded_thenNoThrowAndUnderlyingEncodableProducesExpectedResult() { + // given + let encodableValue = "Test" + let encodableSpy = EncodableSpy() + encodableSpy.encodableValue = encodableValue + let anyEncodable = encodableSpy.eraseToAnyEncodable() + + // when & then + var result: Data = .init() + XCTAssertNoThrow( + result = try JSONEncoder().encode(anyEncodable) + ) + XCTAssertEqual(1, encodableSpy.encodeCallCount) + XCTAssertEqual(#""\#(encodableValue)""#, String(data: result, encoding: .utf8)) + } +} diff --git a/Tests/ConfidentialCoreTests/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+SecureRandomTests.swift b/Tests/ConfidentialCoreTests/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+SecureRandomTests.swift new file mode 100644 index 0000000..a49c444 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+SecureRandomTests.swift @@ -0,0 +1,50 @@ +@testable import ConfidentialCore +import XCTest + +final class FixedWidthInteger_SecureRandomTests: XCTestCase { + + func test_givenFixedWidthIntegerTypes_whenSecureRandom_thenReturnsNonZeroValues() throws { + // given + let types: [Any] = [ + UInt8.self, UInt16.self, UInt32.self, UInt64.self, + Int8.self, Int16.self, Int32.self, Int64.self + ] + + // when + let values = try types.map { try ($0 as! FixedWidthInteger.Type).secureRandom() } + + // then + values.forEach { + XCTAssertNotEqual(.zero, $0.nonzeroBitCount) + } + } + + func test_givenSecureRandomNumberSource_whenSecureRandom_thenReturnsExpectedValue() throws { + // given + let source: FixedWidthInteger.SecureRandomNumberSource = { bytes in + (0.. \(expectedFuncReturnTypeName) { + try \(dataCompressorFullyQualifiedName)(algorithm: .\(compressionAlgorithmStub.name)) + .deobfuscate( + try \(dataShufflerFullyQualifiedName)(nonce: \(randomizationNonceStub)) + .deobfuscate( + try \(dataCrypterFullyQualifiedName)(algorithm: .\(encryptionAlgorithmStub.name)) + .deobfuscate(\(expectedFuncParamName)) + ) + ) + } + """, + .init(describing: functionDeclSyntax) + ) + XCTAssertTrue(algorithm.isEmpty) + } + + func test_givenEmptyAlgorithm_whenParse_thenThrowsExpectedError() { + // given + var algorithm = Algorithm() + let parser = DeobfuscateDataFunctionDeclParser(functionNestingLevel: .zero) + + // when & then + XCTAssertThrowsError(try parser.parse(&algorithm)) { error in + XCTAssertEqual( + "Obfuscation algorithm must consist of at least one obfuscation step.", + "\(error)" + ) + } + } +} diff --git a/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/NamespaceDeclParserTests.swift b/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/NamespaceDeclParserTests.swift new file mode 100644 index 0000000..d5e6b7a --- /dev/null +++ b/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/NamespaceDeclParserTests.swift @@ -0,0 +1,134 @@ +@testable import ConfidentialCore +import XCTest + +import SwiftSyntaxBuilder + +final class NamespaceDeclParserTests: XCTestCase { + + private typealias MembersParserSpy = ParserSpy< + ArraySlice, + [ExpressibleAsMemberDeclListItem] + > + private typealias DeobfuscateDataFunctionDeclParserSpy = ParserSpy< + SourceSpecification.Algorithm, + ExpressibleAsMemberDeclListItem + > + + private let memberNameStub = "tested" + private let memberTypeNameStub = "Bool" + private let deobfuscateDataFunctionNameStub = "test" + + private var membersParserSpy: MembersParserSpy! + private var deobfuscateDataFunctionDeclParserSpy: DeobfuscateDataFunctionDeclParserSpy! + + private var sut: NamespaceDeclParser! + + override func setUp() { + super.setUp() + membersParserSpy = .init(result: membersStub) + membersParserSpy.consumeInput = { $0 = [] } + deobfuscateDataFunctionDeclParserSpy = .init(result: deobfuscateDataFunctionDeclStub) + deobfuscateDataFunctionDeclParserSpy.consumeInput = { $0 = [] } + sut = .init( + membersParser: membersParserSpy, + deobfuscateDataFunctionDeclParser: deobfuscateDataFunctionDeclParserSpy + ) + } + + override func tearDown() { + sut = nil + deobfuscateDataFunctionDeclParserSpy = nil + membersParserSpy = nil + super.tearDown() + } + + func test_givenSourceSpecification_whenParse_thenReturnsExpectedNamespaceDeclarationsAndSourceSpecificationIsEmpty() throws { + // given + let namespaceNameStub = "Secrets" + let secretStub = SourceSpecification.Secret.StubFactory.makeSecret() + var sourceSpecification = SourceSpecification.StubFactory.makeSpecification( + secrets: [ + .create(identifier: namespaceNameStub): [secretStub], + .extend(identifier: namespaceNameStub, moduleName: .none): [secretStub] + ] + ) + + // when + let namespaceDeclarations: [ExpressibleAsCodeBlockItem] = try sut.parse(&sourceSpecification) + + // then + let namespaceDeclarationsSyntax = namespaceDeclarations + .map { $0.createCodeBlockItem() } + .map { $0.buildSyntax(format: .init(indentWidth: .zero)) } + .map { String(describing: $0) } + .sorted() + + XCTAssertEqual( + [ + """ + + enum \(namespaceNameStub) { + + static var \(memberNameStub): \(memberTypeNameStub) = false + + static func \(deobfuscateDataFunctionNameStub)() { + } + } + """, + """ + + extension \(namespaceNameStub) { + + static var \(memberNameStub): \(memberTypeNameStub) = false + + static func \(deobfuscateDataFunctionNameStub)() { + } + } + """ + ], + namespaceDeclarationsSyntax + ) + XCTAssertEqual([[secretStub], [secretStub]], membersParserSpy.parseRecordedInput) + XCTAssertEqual([[]], deobfuscateDataFunctionDeclParserSpy.parseRecordedInput) + XCTAssertTrue(sourceSpecification.algorithm.isEmpty) + XCTAssertTrue(sourceSpecification.secrets.isEmpty) + } +} + +private extension NamespaceDeclParserTests { + + var membersStub: [ExpressibleAsMemberDeclListItem] { + [ + VariableDecl( + modifiers: ModifierList([ + DeclModifier(name: .static.withLeadingTrivia(.newlines(1).appending(.spaces(4)))) + ]), + letOrVarKeyword: .var, + bindings: PatternBindingList([ + PatternBinding( + pattern: IdentifierPattern(memberNameStub), + typeAnnotation: TypeAnnotation(memberTypeNameStub), + initializer: InitializerClause( + value: BooleanLiteralExpr(booleanLiteral: .false.withoutTrivia()) + ) + ) + ]) + ) + ] + } + + var deobfuscateDataFunctionDeclStub: ExpressibleAsMemberDeclListItem { + FunctionDecl( + modifiers: ModifierList([ + DeclModifier(name: .static.withLeadingTrivia(.newlines(1).appending(.spaces(4)))) + ]), + identifier: .identifier(deobfuscateDataFunctionNameStub), + signature: FunctionSignature(input: ParameterClause()), + body: CodeBlock( + leftBrace: .leftBrace.withLeadingTrivia(.spaces(1)), + statements: CodeBlockItemList([]), + rightBrace: .rightBrace.withLeadingTrivia(.spaces(4)) + ) + ) + } +} diff --git a/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/NamespaceMembersParserTests.swift b/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/NamespaceMembersParserTests.swift new file mode 100644 index 0000000..2d3ffdc --- /dev/null +++ b/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/NamespaceMembersParserTests.swift @@ -0,0 +1,91 @@ +@testable import ConfidentialCore +import XCTest + +import SwiftSyntaxBuilder + +final class NamespaceMembersParserTests: XCTestCase { + + private typealias Secret = SourceSpecification.Secret + private typealias SecretDeclParserSpy = ParserSpy + + private let secretDeclStub = SecretDecl( + name: "secret", + dataArgumentExpression: ArrayExpr { + ArrayElement(expression: IntegerLiteralExpr(digits: "0x20"), trailingComma: .comma) + ArrayElement(expression: IntegerLiteralExpr(digits: "0x20")) + }, + dataAccessWrapper: CustomAttribute( + attributeName: SimpleTypeIdentifier("Test"), + argumentList: .none + ) + ) + private let secretsStub: ArraySlice = [ + .StubFactory.makeSecret(), + .StubFactory.makeSecret() + ] + + private var secretDeclParserSpy: SecretDeclParserSpy! + + private var sut: NamespaceMembersParser! + + override func setUp() { + super.setUp() + secretDeclParserSpy = .init(result: secretDeclStub) + sut = .init(secretDeclParser: secretDeclParserSpy) + } + + override func tearDown() { + sut = nil + secretDeclParserSpy = nil + super.tearDown() + } + + func test_givenSecrets_whenParse_thenReturnsExpectedMemberDeclarationsAndSecretsIsEmpty() throws { + // given + var secrets = secretsStub + + // when + let memberDeclarations: [ExpressibleAsMemberDeclListItem] = try sut.parse(&secrets) + + // then + let format = Format(indentWidth: .zero) + let membersSourceText = memberDeclarations + .map { $0.createMemberDeclListItem() } + .map { $0.buildSyntax(format: format) } + .map { String(describing: $0) } + let expectedMembersSourceText = secretsStub + .map { _ in secretDeclStub.buildSyntax(format: format) } + .map { String(describing: $0) } + XCTAssertEqual(expectedMembersSourceText, membersSourceText) + XCTAssertEqual(secretsStub, secretDeclParserSpy.parseRecordedInput[...]) + XCTAssertTrue(secrets.isEmpty) + } + + func test_givenSecretDeclParserFailsOnFirstSecret_whenParse_thenThrowsErrorAndSecretsLeftIntact() { + // given + var secrets = secretsStub + secretDeclParserSpy.consumeInput = { _ in throw ErrorDummy() } + + // when & then + XCTAssertThrowsError(try sut.parse(&secrets)) + XCTAssertEqual([secretsStub.first!], secretDeclParserSpy.parseRecordedInput) + XCTAssertEqual(secretsStub, secrets) + } + + func test_givenSecretDeclParserFailsOnSecondSecret_whenParse_thenThrowsErrorAndSecretsContainAll() { + // given + var secrets = secretsStub + var secretCount: Int = .zero + secretDeclParserSpy.consumeInput = { _ in + guard secretCount == .zero else { + throw ErrorDummy() + } + secretCount += 1 + } + + // when & then + XCTAssertThrowsError(try sut.parse(&secrets)) + XCTAssertEqual(secretsStub, secretDeclParserSpy.parseRecordedInput[...]) + XCTAssertEqual(secretsStub.dropFirst(), secrets) + } +} diff --git a/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/SecretDeclParserTests.swift b/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/SecretDeclParserTests.swift new file mode 100644 index 0000000..deebb6a --- /dev/null +++ b/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/SecretDeclParserTests.swift @@ -0,0 +1,41 @@ +@testable import ConfidentialCore +import XCTest + +import ConfidentialKit + +final class SecretDeclParserTests: XCTestCase { + + private typealias Secret = SourceSpecification.Secret + + func test_givenSecret_whenParse_thenReturnsExpectedSecretDecl() throws { + // given + let deobfuscateArgumentName = "deobfuscate" + let deobfuscateDataFuncName = "deobfuscateData" + let secret = Secret( + name: "secret", + data: .init([0x20, 0x20]), + dataAccessWrapperInfo: .init( + typeInfo: .init(of: Obfuscated.self), + arguments: [(label: deobfuscateArgumentName, value: deobfuscateDataFuncName)] + ) + ) + + // when + let secretDecl = try SecretDeclParser().parse(secret) + + // then + let secretDeclSyntax = secretDecl + .createSecretDecl() + .buildSyntax(format: .init(indentWidth: .zero)) + let expectedAttributeName = secret.dataAccessWrapperInfo.typeInfo.fullyQualifiedName + let expectedDeclTypeName = TypeInfo(of: Obfuscation.Secret.self).fullyQualifiedName + XCTAssertEqual( + """ + + @\(expectedAttributeName)(\(deobfuscateArgumentName): \(deobfuscateDataFuncName)) + static var \(secret.name): \(expectedDeclTypeName) = .init(data: [0x20, 0x20]) + """, + .init(describing: secretDeclSyntax) + ) + } +} diff --git a/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/SourceFileParserTests.swift b/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/SourceFileParserTests.swift new file mode 100644 index 0000000..8404419 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Parsing/Parsers/CodeGeneration/SourceFileParserTests.swift @@ -0,0 +1,88 @@ +@testable import ConfidentialCore +import XCTest + +import SwiftSyntaxBuilder + +final class SourceFileParserTests: XCTestCase { + + private typealias CodeBlockParserSpy = ParserSpy + + private let moduleNameStub = "SecretModule" + private let enumNameStub = "Test" + + private var codeBlockParserSpy: CodeBlockParserSpy! + + private var sut: SourceFileParser! + + override func setUp() { + super.setUp() + codeBlockParserSpy = .init(result: codeBlockStub) + codeBlockParserSpy.consumeInput = { + $0.algorithm = [] + $0.secrets = [:] + } + sut = .init { + codeBlockParserSpy! + } + } + + override func tearDown() { + sut = nil + codeBlockParserSpy = nil + super.tearDown() + } + + func test_givenSourceSpecification_whenParse_thenReturnsExpectedSourceFileTextAndSourceSpecificationIsEmpty() throws { + // given + var sourceSpecification = SourceSpecification.StubFactory.makeSpecification( + secrets: [ + .create(identifier: "Secrets"): [], + .extend(identifier: "Secret", moduleName: moduleNameStub): [] + ] + ) + + // when + let sourceFileText: SourceFileText = try sut.parse(&sourceSpecification) + + // then + XCTAssertEqual( + """ + + import ConfidentialKit + import Foundation + import \(moduleNameStub) + + enum \(enumNameStub) { + } + """, + .init(describing: sourceFileText) + ) + XCTAssertTrue(sourceSpecification.algorithm.isEmpty) + XCTAssertTrue(sourceSpecification.secrets.isEmpty) + } + + func test_givenCodeBlockParserFails_whenParse_thenThrowsError() { + // given + var sourceSpecification = SourceSpecification.StubFactory.makeSpecification() + codeBlockParserSpy.consumeInput = { _ in throw ErrorDummy() } + + // when & then + XCTAssertThrowsError(try sut.parse(&sourceSpecification)) + } +} + +private extension SourceFileParserTests { + + var codeBlockStub: [ExpressibleAsCodeBlockItem] { + [ + EnumDecl( + enumKeyword: .enum.withLeadingTrivia(.newlines(1)), + identifier: .identifier(enumNameStub), + members: MemberDeclBlock( + leftBrace: .leftBrace.withLeadingTrivia(.spaces(1)), + members: MemberDeclList([]) + ) + ) + ] + } +} diff --git a/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/AlgorithmParserTests.swift b/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/AlgorithmParserTests.swift new file mode 100644 index 0000000..f4fb743 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/AlgorithmParserTests.swift @@ -0,0 +1,81 @@ +@testable import ConfidentialCore +import XCTest + +final class AlgorithmParserTests: XCTestCase { + + private typealias ObfuscationStepParserSpy = ParserSpy + + private let obfuscationStepStub = "test" + private lazy var algorithmStub = (0..<2).map { _ in obfuscationStepStub }[...] + + private var obfuscationStepParserSpy: ObfuscationStepParserSpy! + + private var sut: AlgorithmParser! + + override func setUp() { + super.setUp() + obfuscationStepParserSpy = .init(result: .init(technique: .compression(algorithm: .lzfse))) + obfuscationStepParserSpy.consumeInput = { $0 = "" } + sut = .init(obfuscationStepParser: obfuscationStepParserSpy) + } + + override func tearDown() { + sut = nil + obfuscationStepParserSpy = nil + super.tearDown() + } + + func test_givenConfiguration_whenParse_thenReturnsExpectedValueAndConfigurationAlgorithmIsEmpty() throws { + // given + var configuration = Configuration.StubFactory.makeConfiguration(algorithm: algorithmStub) + + // when + let algorithm = try sut.parse(&configuration) + + // then + XCTAssertEqual( + (0.. + + private let inputStub = "test"[...] + + private var compressionTechniqueParserSpy: TechniqueParserSpy! + private var randomizationTechniqueParserSpy: TechniqueParserSpy! + + private var sut: ObfuscationStepParser>! + + override func setUp() { + super.setUp() + compressionTechniqueParserSpy = .init(result: .compression(algorithm: .lz4)) + randomizationTechniqueParserSpy = .init(result: .randomization(nonce: 123456789)) + sut = ObfuscationStepParser { + compressionTechniqueParserSpy! + randomizationTechniqueParserSpy! + } + } + + override func tearDown() { + sut = nil + randomizationTechniqueParserSpy = nil + compressionTechniqueParserSpy = nil + super.tearDown() + } + + func test_givenFirstTechniqueParserSucceeds_whenParse_thenReturnsExpectedValue() throws { + // given + var input = inputStub + compressionTechniqueParserSpy.consumeInput = { _ in } + randomizationTechniqueParserSpy.consumeInput = { _ in throw ErrorDummy() } + + // when + let obfuscationStep = try sut.parse(&input) + + // then + XCTAssertEqual(.init(technique: compressionTechniqueParserSpy.result), obfuscationStep) + XCTAssertEqual([inputStub], compressionTechniqueParserSpy.parseRecordedInput) + XCTAssertEqual([], randomizationTechniqueParserSpy.parseRecordedInput) + } + + func test_givenSecondTechniqueParserSucceeds_whenParse_thenReturnsExpectedValue() throws { + // given + var input = inputStub + compressionTechniqueParserSpy.consumeInput = { _ in throw ErrorDummy() } + randomizationTechniqueParserSpy.consumeInput = { _ in } + + // when + let obfuscationStep = try sut.parse(&input) + + // then + XCTAssertEqual(.init(technique: randomizationTechniqueParserSpy.result), obfuscationStep) + XCTAssertEqual([inputStub], compressionTechniqueParserSpy.parseRecordedInput) + XCTAssertEqual([inputStub], randomizationTechniqueParserSpy.parseRecordedInput) + } + + func test_givenBothTechniqueParsersFail_whenParse_thenThrowsError() { + // given + var input = inputStub + compressionTechniqueParserSpy.consumeInput = { _ in throw ErrorDummy() } + randomizationTechniqueParserSpy.consumeInput = { _ in throw ErrorDummy() } + + // when & then + XCTAssertThrowsError(try sut.parse(&input)) + XCTAssertEqual([inputStub], compressionTechniqueParserSpy.parseRecordedInput) + XCTAssertEqual([inputStub], randomizationTechniqueParserSpy.parseRecordedInput) + } +} diff --git a/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/RandomizationTechniqueParserTests.swift b/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/RandomizationTechniqueParserTests.swift new file mode 100644 index 0000000..0fc7ae6 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/RandomizationTechniqueParserTests.swift @@ -0,0 +1,70 @@ +@testable import ConfidentialCore +import XCTest + +final class RandomizationTechniqueParserTests: XCTestCase { + + private let nonceStub: UInt64 = 123456789 + + private var generateNonceSpy: ParameterlessClosureSpy! + + private var sut: RandomizationTechniqueParser! + + override func setUp() { + super.setUp() + generateNonceSpy = .init(result: nonceStub) + sut = .init(generateNonce: generateNonceSpy.closureWithError) + } + + override func tearDown() { + sut = nil + generateNonceSpy = nil + super.tearDown() + } + + func test_givenValidInput_whenParse_thenReturnsExpectedEnumValueAndInputIsEmpty() throws { + // given + var input = " \(C.Parsing.Keywords.shuffle)"[...] + + // when + let technique = try sut.parse(&input) + + // then + XCTAssertEqual(.randomization(nonce: nonceStub), technique) + XCTAssertEqual(1, generateNonceSpy.callCount) + XCTAssertTrue(input.isEmpty) + } + + func test_givenInvalidInput_whenParse_thenThrowsErrorAndInputLeftIntact() { + // given + var input = "\(C.Parsing.Keywords.compress)"[...] + + // when & then + XCTAssertThrowsError(try sut.parse(&input)) + XCTAssertEqual(.zero, generateNonceSpy.callCount) + XCTAssertEqual("\(C.Parsing.Keywords.compress)", input) + } + + func test_givenInputWithUnexpectedTrailingData_whenParse_thenThrowsErrorAndInputEqualsTrailingData() { + // given + var input = "\(C.Parsing.Keywords.shuffle) Test"[...] + + // when & then + XCTAssertThrowsError(try sut.parse(&input)) + XCTAssertEqual(.zero, generateNonceSpy.callCount) + XCTAssertEqual(" Test", input) + } + + func test_givenValidInputAndGenerateNonceError_whenParse_thenThrowsExpectedErrorAndInputIsEmpty() { + // given + var input = "\(C.Parsing.Keywords.shuffle)"[...] + let error = ErrorDummy() + generateNonceSpy.error = error + + // when & then + XCTAssertThrowsError(try sut.parse(&input)) { + XCTAssertEqual(error, $0 as? ErrorDummy) + } + XCTAssertEqual(1, generateNonceSpy.callCount) + XCTAssertTrue(input.isEmpty) + } +} diff --git a/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/SecretNamespaceParserTests.swift b/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/SecretNamespaceParserTests.swift new file mode 100644 index 0000000..7dd1c01 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/SecretNamespaceParserTests.swift @@ -0,0 +1,81 @@ +@testable import ConfidentialCore +import XCTest + +final class SecretNamespaceParserTests: XCTestCase { + + private typealias Namespace = SecretNamespaceParser.Namespace + + private let secretsNamespaceStub = "Secrets" + private let secretModuleStub = "SecretModule" + + private var sut: SecretNamespaceParser! + + override func setUp() { + super.setUp() + sut = .init() + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + func test_givenValidInputData_whenParse_thenReturnsExpectedEnumValuesAndInputIsEmpty() throws { + // given + var inputData = [ + ""[...], + "\(C.Parsing.Keywords.create) \(secretsNamespaceStub)"[...], + "\(C.Parsing.Keywords.extend) \(secretsNamespaceStub)"[...], + "\(C.Parsing.Keywords.extend) \(secretsNamespaceStub) \(C.Parsing.Keywords.from) \(secretModuleStub)" + ] + + // when + let namespaces = try inputData.indices.map { + try sut.parse(&inputData[$0]) + } + + // then + let expectedNamespaces: [Namespace] = [ + .extend(identifier: "ConfidentialKit.Obfuscation.Secret", moduleName: "ConfidentialKit"), + .create(identifier: secretsNamespaceStub), + .extend(identifier: secretsNamespaceStub, moduleName: .none), + .extend(identifier: secretsNamespaceStub, moduleName: secretModuleStub) + ] + XCTAssertEqual(inputData.count, namespaces.count) + XCTAssertEqual(expectedNamespaces.count, namespaces.count) + namespaces.enumerated().forEach { idx, namespace in + XCTAssertEqual(expectedNamespaces[idx], namespace) + XCTAssertTrue(inputData[idx].isEmpty) + } + } + + func test_givenValidInputWithExtraWhitespaces_whenParse_thenReturnsExpectedEnumValueAndInputIsEmpty() throws { + // given + var input = " \(C.Parsing.Keywords.extend) \(secretsNamespaceStub) \(C.Parsing.Keywords.from) \(secretModuleStub)"[...] + + // when + let namespace = try sut.parse(&input) + + // then + XCTAssertEqual(.extend(identifier: secretsNamespaceStub, moduleName: secretModuleStub), namespace) + XCTAssertTrue(input.isEmpty) + } + + func test_givenInvalidInput_whenParse_thenThrowsErrorAndInputLeftIntact() { + // given + var input = "\(C.Parsing.Keywords.shuffle)"[...] + + // when & then + XCTAssertThrowsError(try sut.parse(&input)) + XCTAssertEqual("\(C.Parsing.Keywords.shuffle)", input) + } + + func test_givenInputWithUnexpectedTrailingData_whenParse_thenThrowsErrorAndInputEqualsTrailingData() { + // given + var input = "\(C.Parsing.Keywords.create) \(secretsNamespaceStub) Test"[...] + + // when & then + XCTAssertThrowsError(try sut.parse(&input)) + XCTAssertEqual(" Test", input) + } +} diff --git a/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/SecretsParserTests.swift b/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/SecretsParserTests.swift new file mode 100644 index 0000000..6a11a29 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Parsing/Parsers/ModelTransform/SecretsParserTests.swift @@ -0,0 +1,103 @@ +@testable import ConfidentialCore +import XCTest + +final class SecretsParserTests: XCTestCase { + + private typealias Namespace = SourceSpecification.Secret.Namespace + private typealias NamespaceParserSpy = ParserSpy + private typealias Secrets = SourceSpecification.Secrets + + private let secretsNamespaceStub: Namespace = .create(identifier: "Secrets") + private let secretsStub: ArraySlice = [ + .init(name: "secret1", value: .singleValue("message"), namespace: .none), + .init(name: "secret2", value: .array(["mess", "age"]), namespace: .none) + ] + + private var namespaceParserSpy: NamespaceParserSpy! + private var secretValueEncoderSpy: DataEncoderSpy! + + private var sut: SecretsParser! + + override func setUp() { + super.setUp() + namespaceParserSpy = .init(result: secretsNamespaceStub) + namespaceParserSpy.consumeInput = { $0 = "" } + secretValueEncoderSpy = .init(underlyingEncoder: JSONEncoder()) + sut = .init( + namespaceParser: namespaceParserSpy, + secretValueEncoder: secretValueEncoderSpy + ) + } + + override func tearDown() { + sut = nil + secretValueEncoderSpy = nil + namespaceParserSpy = nil + super.tearDown() + } + + func test_givenConfiguration_whenParse_thenReturnsExpectedValueAndConfigurationSecretsIsEmpty() throws { + // given + var configuration = Configuration.StubFactory.makeConfiguration(secrets: secretsStub) + + // when + let secrets: Secrets = try sut.parse(&configuration) + + // then + let expectedSecrets: SourceSpecification.Secrets = [ + secretsNamespaceStub: try secretsStub.map { + try SourceSpecification.Secret.StubFactory.makeSecret( + from: $0, + using: secretValueEncoderSpy.underlyingEncoder + ) + }[...] + ] + XCTAssertEqual(expectedSecrets, secrets) + XCTAssertEqual( + ["Obfuscated", "Obfuscated>"], + secrets[secretsNamespaceStub]?.map { $0.dataAccessWrapperInfo.typeInfo.name } + ) + XCTAssertEqual((0.. + private typealias SecretsParserSpy = ParserSpy + + private let configurationStub: Configuration = { + var configuration = Configuration.StubFactory.makeConfiguration() + configuration.defaultNamespace = "Secrets" + return configuration + }() + private let algorithmStub: SourceSpecification.Algorithm = [ + .init(technique: .encryption(algorithm: .chaChaPoly)) + ] + private let secretsStub: SourceSpecification.Secrets = [ + .extend(identifier: "Secrets", moduleName: "SecretModule"): [.StubFactory.makeSecret()] + ] + + private var algorithmParserSpy: AlgorithmParserSpy! + private var secretsParserSpy: SecretsParserSpy! + + private var sut: SourceSpecificationParser! + + override func setUp() { + super.setUp() + algorithmParserSpy = .init(result: algorithmStub) + algorithmParserSpy.consumeInput = { $0.algorithm = [] } + secretsParserSpy = .init(result: secretsStub) + secretsParserSpy.consumeInput = { $0.secrets = [] } + sut = .init( + algorithmParser: algorithmParserSpy, + secretsParser: secretsParserSpy + ) + } + + override func tearDown() { + sut = nil + secretsParserSpy = nil + algorithmParserSpy = nil + super.tearDown() + } + + func test_givenConfiguration_whenParse_thenReturnsExpectedValueAndConfigurationDefaultNamespaceIsNil() throws { + // given + var configuration = configurationStub + + // when + let sourceSpecification: SourceSpecification = try sut.parse(&configuration) + + // then + XCTAssertEqual(.init(algorithm: algorithmStub, secrets: secretsStub), sourceSpecification) + XCTAssertEqual([configurationStub], algorithmParserSpy.parseRecordedInput) + XCTAssertEqual([configurationStub], secretsParserSpy.parseRecordedInput) + XCTAssertNil(configuration.defaultNamespace) + } + + func test_givenAlgorithmParserFails_whenParse_thenThrowsError() { + // given + var configuration = configurationStub + algorithmParserSpy.consumeInput = { _ in throw ErrorDummy() } + + // when & then + XCTAssertThrowsError(try sut.parse(&configuration)) + } + + func test_givenSecretsParserFails_whenParse_thenThrowsError() { + // given + var configuration = configurationStub + secretsParserSpy.consumeInput = { _ in throw ErrorDummy() } + + // when & then + XCTAssertThrowsError(try sut.parse(&configuration)) + } +} diff --git a/Tests/ConfidentialCoreTests/SyntaxText/SourceFileTextTests.swift b/Tests/ConfidentialCoreTests/SyntaxText/SourceFileTextTests.swift new file mode 100644 index 0000000..0c5600e --- /dev/null +++ b/Tests/ConfidentialCoreTests/SyntaxText/SourceFileTextTests.swift @@ -0,0 +1,68 @@ +@testable import ConfidentialCore +import XCTest + +import SwiftSyntaxBuilder + +final class SourceFileTextTests: XCTestCase { + + private var temporaryFileURL: URL! + + override func setUp() { + super.setUp() + temporaryFileURL = .init(fileURLWithPath: NSTemporaryDirectory()) + .appendingPathComponent("\(UUID().uuidString).swift") + } + + override func tearDownWithError() throws { + try FileManager.default.removeItem(at: temporaryFileURL) + temporaryFileURL = nil + super.tearDown() + } + + func test_givenSourceFileTextWithSourceFileSyntax_whenWriteToFile_thenFileContainsExpectedSyntaxText() throws { + // given + let sourceFile = SourceFile(eofToken: .eof) { + ImportDecl( + path: AccessPath([AccessPathComponent(name: .identifier("Foundation"))]) + ) + StructDecl( + structKeyword: .struct.withLeadingTrivia(.newlines(1)), + identifier: "Test", + members: MemberDeclBlock( + leftBrace: .leftBrace.withLeadingTrivia(.spaces(1)), + rightBrace: .rightBrace + ) { + VariableDecl( + .let.withLeadingTrivia(.spaces(2)), + name: IdentifierPattern("id"), + type: TypeAnnotation("UUID") + ) + VariableDecl( + .var.withLeadingTrivia(.spaces(2)), + name: IdentifierPattern("data"), + type: TypeAnnotation("Data") + ) + } + ) + } + let sourceFileText = SourceFileText(from: sourceFile) + let encoding = String.Encoding.utf8 + + // when + try sourceFileText.write(to: temporaryFileURL, encoding: encoding) + + // then + let fileContents = try String(contentsOf: temporaryFileURL, encoding: encoding) + XCTAssertEqual( + """ + import Foundation + + struct Test { + let id: UUID + var data: Data + } + """, + fileContents + ) + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Dummies/ErrorDummy.swift b/Tests/ConfidentialCoreTests/TestDoubles/Dummies/ErrorDummy.swift new file mode 100644 index 0000000..e05305a --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Dummies/ErrorDummy.swift @@ -0,0 +1 @@ +struct ErrorDummy: Error, Equatable {} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Spies/DataEncoderSpy.swift b/Tests/ConfidentialCoreTests/TestDoubles/Spies/DataEncoderSpy.swift new file mode 100644 index 0000000..70671f3 --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Spies/DataEncoderSpy.swift @@ -0,0 +1,18 @@ +@testable import ConfidentialCore +import Foundation + +final class DataEncoderSpy: DataEncoder { + + var underlyingEncoder: DataEncoder + + private(set) var encodeRecordedValues: [any Encodable] = [] + + init(underlyingEncoder: DataEncoder) { + self.underlyingEncoder = underlyingEncoder + } + + func encode(_ value: E) throws -> Data { + encodeRecordedValues.append(value) + return try underlyingEncoder.encode(value) + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Spies/EncodableSpy.swift b/Tests/ConfidentialCoreTests/TestDoubles/Spies/EncodableSpy.swift new file mode 100644 index 0000000..523b06b --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Spies/EncodableSpy.swift @@ -0,0 +1,13 @@ +final class EncodableSpy: Encodable { + + var encodableValue: Value? + + private(set) var encodeCallCount: Int = .zero + + func encode(to encoder: Encoder) throws { + encodeCallCount += 1 + guard let encodableValue = encodableValue else { return } + var container = encoder.singleValueContainer() + try container.encode(encodableValue) + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Spies/ObfuscationStepResolverSpy.swift b/Tests/ConfidentialCoreTests/TestDoubles/Spies/ObfuscationStepResolverSpy.swift new file mode 100644 index 0000000..485a7a5 --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Spies/ObfuscationStepResolverSpy.swift @@ -0,0 +1,17 @@ +@testable import ConfidentialCore + +final class ObfuscationStepResolverSpy: DataObfuscationStepResolver { + + var obfuscationStepReturnValue: any DataObfuscationStep + + private(set) var recordedTechniques: [Technique] = [] + + init(obfuscationStepReturnValue: any DataObfuscationStep) { + self.obfuscationStepReturnValue = obfuscationStepReturnValue + } + + func obfuscationStep(for technique: Technique) -> DataObfuscationStep { + recordedTechniques.append(technique) + return obfuscationStepReturnValue + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Spies/ObfuscationStepSpy.swift b/Tests/ConfidentialCoreTests/TestDoubles/Spies/ObfuscationStepSpy.swift new file mode 100644 index 0000000..acbdc0b --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Spies/ObfuscationStepSpy.swift @@ -0,0 +1,14 @@ +@testable import ConfidentialCore +import Foundation + +final class ObfuscationStepSpy: DataObfuscationStep { + + var obfuscateReturnValue: Data? + + private(set) var recordedData: [Data] = [] + + func obfuscate(_ data: Data) throws -> Data { + recordedData.append(data) + return obfuscateReturnValue ?? data + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Spies/ParameterlessClosureSpy.swift b/Tests/ConfidentialCoreTests/TestDoubles/Spies/ParameterlessClosureSpy.swift new file mode 100644 index 0000000..bf62d01 --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Spies/ParameterlessClosureSpy.swift @@ -0,0 +1,22 @@ +final class ParameterlessClosureSpy { + + var result: Result + var error: Error? + + private(set) var callCount: Int = .zero + + init(result: Result) { + self.result = result + } + + func closure() -> Result { + callCount += 1 + return result + } + + func closureWithError() throws -> Result { + callCount += 1 + if let error = error { throw error } + return result + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Spies/ParserSpy.swift b/Tests/ConfidentialCoreTests/TestDoubles/Spies/ParserSpy.swift new file mode 100644 index 0000000..6001227 --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Spies/ParserSpy.swift @@ -0,0 +1,19 @@ +import Parsing + +final class ParserSpy: Parser { + + var result: Output + var consumeInput: ((inout Input) throws -> Void)? + + private(set) var parseRecordedInput: [Input] = [] + + init(result: Output) { + self.result = result + } + + func parse(_ input: inout Input) throws -> Output { + parseRecordedInput.append(input) + try consumeInput?(&input) + return result + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Stubs/ConfigurationStubFactory.swift b/Tests/ConfidentialCoreTests/TestDoubles/Stubs/ConfigurationStubFactory.swift new file mode 100644 index 0000000..2099b03 --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Stubs/ConfigurationStubFactory.swift @@ -0,0 +1,18 @@ +@testable import ConfidentialCore + +extension Configuration { + + enum StubFactory { + + static func makeConfiguration( + algorithm: ArraySlice = [], + secrets: ArraySlice = [] + ) -> Configuration { + .init( + algorithm: algorithm, + defaultNamespace: .none, + secrets: secrets + ) + } + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Stubs/SourceSpecification.SecretStubFactory.swift b/Tests/ConfidentialCoreTests/TestDoubles/Stubs/SourceSpecification.SecretStubFactory.swift new file mode 100644 index 0000000..3b64a8c --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Stubs/SourceSpecification.SecretStubFactory.swift @@ -0,0 +1,24 @@ +@testable import ConfidentialCore +import Foundation + +extension SourceSpecification.Secret { + + enum StubFactory { + + static func makeSecret(named name: String = "secret", with data: Data = .init()) -> SourceSpecification.Secret { + .init( + name: name, + data: data, + dataAccessWrapperInfo: .init(typeInfo: TypeInfo(of: Any.self), arguments: []) + ) + } + + static func makeSecret(from secret: Configuration.Secret, using encoder: DataEncoder) throws -> SourceSpecification.Secret { + .init( + name: secret.name, + data: try encoder.encode(secret.value.underlyingValue), + dataAccessWrapperInfo: .init(typeInfo: TypeInfo(of: Any.self), arguments: []) + ) + } + } +} diff --git a/Tests/ConfidentialCoreTests/TestDoubles/Stubs/SourceSpecificationStubFactory.swift b/Tests/ConfidentialCoreTests/TestDoubles/Stubs/SourceSpecificationStubFactory.swift new file mode 100644 index 0000000..e20525f --- /dev/null +++ b/Tests/ConfidentialCoreTests/TestDoubles/Stubs/SourceSpecificationStubFactory.swift @@ -0,0 +1,14 @@ +@testable import ConfidentialCore + +extension SourceSpecification { + + enum StubFactory { + + static func makeSpecification( + algorithm: SourceSpecification.Algorithm = [], + secrets: SourceSpecification.Secrets = [:] + ) -> SourceSpecification { + .init(algorithm: algorithm, secrets: secrets) + } + } +} diff --git a/Tests/ConfidentialCoreTests/Utils/TypeInfoTests.swift b/Tests/ConfidentialCoreTests/Utils/TypeInfoTests.swift new file mode 100644 index 0000000..0103d96 --- /dev/null +++ b/Tests/ConfidentialCoreTests/Utils/TypeInfoTests.swift @@ -0,0 +1,93 @@ +@testable import ConfidentialCore +import XCTest + +final class TypeInfoTests: XCTestCase { + + func test_givenNestedTypeInfo_whenFullyQualifiedName_thenReturnsExpectedValue() { + // given + let typeInfo = TypeInfo(of: Swift.String.Encoding.self) + + // when + let fullyQualifiedName = typeInfo.fullyQualifiedName + + // then + XCTAssertEqual("Swift.String.Encoding", fullyQualifiedName) + } + + func test_givenNestedTypeInfo_whenModuleName_thenReturnsExpectedValue() { + // given + let typeInfo = TypeInfo(of: Swift.String.Encoding.self) + + // when + let moduleName = typeInfo.moduleName + + // then + XCTAssertEqual("Swift", moduleName) + } + + func test_givenNestedTypeInfo_whenFullName_thenReturnsExpectedValue() { + // given + let typeInfo = TypeInfo(of: Swift.String.Encoding.self) + + // when + let fullName = typeInfo.fullName + + // then + XCTAssertEqual("String.Encoding", fullName) + } + + func test_givenNestedTypeInfo_whenName_thenReturnsExpectedValue() { + // given + let typeInfo = TypeInfo(of: Swift.String.Encoding.self) + + // when + let name = typeInfo.name + + // then + XCTAssertEqual("Encoding", name) + } + + func test_givenTopLevelTypeInfo_whenFullyQualifiedName_thenReturnsExpectedValue() { + // given + let typeInfo = TypeInfo(of: Foundation.Data.self) + + // when + let fullyQualifiedName = typeInfo.fullyQualifiedName + + // then + XCTAssertEqual("Foundation.Data", fullyQualifiedName) + } + + func test_givenTopLevelTypeInfo_whenModuleName_thenReturnsExpectedValue() { + // given + let typeInfo = TypeInfo(of: Foundation.Data.self) + + // when + let moduleName = typeInfo.moduleName + + // then + XCTAssertEqual("Foundation", moduleName) + } + + func test_givenTopLevelTypeInfo_whenFullName_thenReturnsExpectedValue() { + // given + let typeInfo = TypeInfo(of: Foundation.Data.self) + + // when + let fullName = typeInfo.fullName + + // then + XCTAssertEqual("Data", fullName) + } + + func test_givenTopLevelTypeInfo_whenName_thenReturnsExpectedValue() { + // given + let typeInfo = TypeInfo(of: Foundation.Data.self) + + // when + let name = typeInfo.name + + // then + XCTAssertEqual("Data", name) + } +} diff --git a/Tests/ConfidentialKitTests/Extensions/CryptoKit/SymmetricKeySize/SymmetricKeySize+ByteCountTests.swift b/Tests/ConfidentialKitTests/Extensions/CryptoKit/SymmetricKeySize/SymmetricKeySize+ByteCountTests.swift new file mode 100644 index 0000000..d5d0163 --- /dev/null +++ b/Tests/ConfidentialKitTests/Extensions/CryptoKit/SymmetricKeySize/SymmetricKeySize+ByteCountTests.swift @@ -0,0 +1,29 @@ +@testable import ConfidentialKit +import XCTest + +import CryptoKit + +final class SymmetricKeySize_ByteCountTests: XCTestCase { + + func test_givenStandardKeySize_whenByteCount_thenReturnsExpectedValue() { + // given + let keySize = SymmetricKeySize.bits192 + + // when + let byteCount = keySize.byteCount + + // then + XCTAssertEqual(24, byteCount) + } + + func test_givenNonStandardKeySize_whenByteCount_thenReturnsExpectedValue() { + // given + let keySize = SymmetricKeySize(bitCount: 72) + + // when + let byteCount = keySize.byteCount + + // then + XCTAssertEqual(9, byteCount) + } +} diff --git a/Tests/ConfidentialKitTests/Extensions/Swift/BinaryInteger/BinaryInteger+BytesTests.swift b/Tests/ConfidentialKitTests/Extensions/Swift/BinaryInteger/BinaryInteger+BytesTests.swift new file mode 100644 index 0000000..5233099 --- /dev/null +++ b/Tests/ConfidentialKitTests/Extensions/Swift/BinaryInteger/BinaryInteger+BytesTests.swift @@ -0,0 +1,58 @@ +@testable import ConfidentialKit +import XCTest + +final class BinaryInteger_BytesTests: XCTestCase { + + func test_givenBinaryIntegers_whenBytes_thenReturnsExpectedValues() { + // given + let integers: [Any] = [ + UInt8(1), UInt16(1), UInt32(1), UInt64(1), + Int8(1), Int16(1), Int32(1), Int64(1) + ] + + // when + let bytes = integers.map { ($0 as! FixedWidthInteger).littleEndian.bytes } + + // then + XCTAssertEqual( + [ + [0x01], [0x01, 0x00], [0x01, 0x00, 0x00, 0x00], [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + [0x01], [0x01, 0x00], [0x01, 0x00, 0x00, 0x00], [0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ], + bytes + ) + } + + func test_givenCollectionOfTwoBytes_whenUInt8InitBytes_thenReturnsUInt8WithExpectedValue() { + // given + let bytes: [UInt8] = [0x01, 0x01] + + // when + let uInt8 = UInt8(bytes: bytes) + + // then + XCTAssertEqual(1, uInt8) + } + + func test_givenCollectionOfTwoBytes_whenUInt32InitBytes_thenReturnsUInt32WithExpectedValue() { + // given + let bytes: [UInt8] = [0x01, 0x01] + + // when + let uInt32 = UInt32(bytes: bytes) + + // then + XCTAssertEqual(257, uInt32) + } + + func test_givenEmptyCollection_whenIntInitBytes_thenReturnsIntWithValueEqualToZero() { + // given + let bytes: [UInt8] = [] + + // when + let int = Int(bytes: bytes) + + // then + XCTAssertEqual(.zero, int) + } +} diff --git a/Tests/ConfidentialKitTests/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+ByteWidthTests.swift b/Tests/ConfidentialKitTests/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+ByteWidthTests.swift new file mode 100644 index 0000000..0509d01 --- /dev/null +++ b/Tests/ConfidentialKitTests/Extensions/Swift/FixedWidthInteger/FixedWidthInteger+ByteWidthTests.swift @@ -0,0 +1,25 @@ +@testable import ConfidentialKit +import XCTest + +final class FixedWidthInteger_ByteWidthTests: XCTestCase { + + func test_givenFixedWidthIntegerTypes_whenByteWidth_thenReturnsExpectedValues() { + // given + let types: [Any] = [ + UInt8.self, UInt16.self, UInt32.self, UInt64.self, + Int8.self, Int16.self, Int32.self, Int64.self + ] + + // when + let byteWidths = types.map { ($0 as! FixedWidthInteger.Type).byteWidth } + + // then + XCTAssertEqual( + [ + 1, 2, 4, 8, + 1, 2, 4, 8 + ], + byteWidths + ) + } +} diff --git a/Tests/ConfidentialKitTests/Obfuscation/Obfuscation+SecretTests.swift b/Tests/ConfidentialKitTests/Obfuscation/Obfuscation+SecretTests.swift new file mode 100644 index 0000000..6fa4a81 --- /dev/null +++ b/Tests/ConfidentialKitTests/Obfuscation/Obfuscation+SecretTests.swift @@ -0,0 +1,28 @@ +@testable import ConfidentialKit +import XCTest + +final class Obfuscation_SecretTests: XCTestCase { + + func test_givenSecretWithTestBytes_whenData_thenReturnsTestBytes() { + // given + let testBytes: [UInt8] = [0x35, 0x9d, 0x82, 0x1f, 0x68, 0x81, 0x02, 0x64] + let secret = Obfuscation.Secret(data: testBytes) + + // when + let data = secret.data + + // then + XCTAssertEqual(testBytes, data) + } + + func test_givenSecretWithEmptyByteArray_whenData_thenReturnsEmptyArray() { + // given + let secret = Obfuscation.Secret(data: []) + + // when + let data = secret.data + + // then + XCTAssertTrue(data.isEmpty) + } +} diff --git a/Tests/ConfidentialKitTests/Obfuscation/PropertyWrappers/ObfuscatedTests.swift b/Tests/ConfidentialKitTests/Obfuscation/PropertyWrappers/ObfuscatedTests.swift new file mode 100644 index 0000000..d785729 --- /dev/null +++ b/Tests/ConfidentialKitTests/Obfuscation/PropertyWrappers/ObfuscatedTests.swift @@ -0,0 +1,65 @@ +@testable import ConfidentialKit +import XCTest + +final class ObfuscatedTests: XCTestCase { + + private let secretPlainValue: SingleValue = .StubFactory.makeSecretMessage() + + private var secretStub: Obfuscation.Secret! + private var deobfuscateDataSpy: DeobfuscateDataFuncSpy! + private var dataDecoderSpy: DataDecoderSpy! + + private var sut: Obfuscated! + + override func setUp() { + super.setUp() + secretStub = .StubFactory.makeJSONEncodedSecret(with: secretPlainValue) + deobfuscateDataSpy = .init() + dataDecoderSpy = .init(underlyingDecoder: JSONDecoder()) + sut = .init( + wrappedValue: secretStub, + deobfuscateData: deobfuscateDataSpy.deobfuscateData, + decoder: dataDecoderSpy + ) + } + + override func tearDown() { + sut = nil + dataDecoderSpy = nil + deobfuscateDataSpy = nil + secretStub = nil + super.tearDown() + } + + func test_whenWrappedValue_thenReturnsExpectedSecretStub() { + // when + let wrappedValue = sut.wrappedValue + + // then + XCTAssertEqual(secretStub, wrappedValue) + } + + func test_whenProjectedValue_thenReturnsExpectedPlainValue() { + // when + let projectedValue = sut.projectedValue + + // then + XCTAssertEqual(secretPlainValue, projectedValue) + } + + func test_whenProjectedValue_thenDeobfuscateDataFuncCalledOnce() { + // when + _ = sut.projectedValue + + // then + XCTAssertEqual([.init(secretStub.data)], deobfuscateDataSpy.recordedData) + } + + func test_whenProjectedValue_thenDataDecoderCalledOnce() { + // when + _ = sut.projectedValue + + // then + XCTAssertEqual([.init(secretStub.data)], dataDecoderSpy.decodeRecordedData) + } +} diff --git a/Tests/ConfidentialKitTests/Obfuscation/Techniques/Compression/DataCompressorTests.swift b/Tests/ConfidentialKitTests/Obfuscation/Techniques/Compression/DataCompressorTests.swift new file mode 100644 index 0000000..cc49af1 --- /dev/null +++ b/Tests/ConfidentialKitTests/Obfuscation/Techniques/Compression/DataCompressorTests.swift @@ -0,0 +1,33 @@ +@testable import ConfidentialKit +import XCTest + +final class DataCompressorTests: XCTestCase { + + private typealias Algorithm = Obfuscation.Compression.CompressionAlgorithm + private typealias DataCompressor = Obfuscation.Compression.DataCompressor + + private let plainData: Data = SingleValue.StubFactory.makeSecretMessageData() + private let compressionAlgorithms: [Algorithm] = [ + .lzfse, .lz4, .lzma, .zlib + ] + + func test_givenCompressedData_whenDeobfuscate_thenReturnsDecompressedData() throws { + // given + let compressedData = compressionAlgorithms + .map { algorithm in + ( + algorithm, + Data(referencing: try! NSData(data: plainData).compressed(using: algorithm)) + ) + } + + // when + let decompressedData = try compressedData + .map { algorithm, data in + try DataCompressor(algorithm: algorithm).deobfuscate(data) + } + + // then + decompressedData.forEach { XCTAssertEqual(plainData, $0) } + } +} diff --git a/Tests/ConfidentialKitTests/Obfuscation/Techniques/Encryption/DataCrypterTests.swift b/Tests/ConfidentialKitTests/Obfuscation/Techniques/Encryption/DataCrypterTests.swift new file mode 100644 index 0000000..4a85252 --- /dev/null +++ b/Tests/ConfidentialKitTests/Obfuscation/Techniques/Encryption/DataCrypterTests.swift @@ -0,0 +1,50 @@ +@testable import ConfidentialKit +import XCTest + +import CryptoKit + +final class DataCrypterTests: XCTestCase { + + private typealias Algorithm = Obfuscation.Encryption.SymmetricEncryptionAlgorithm + private typealias DataCrypter = Obfuscation.Encryption.DataCrypter + + private let plainData: Data = SingleValue.StubFactory.makeSecretMessageData() + private let encryptionAlgorithms: [Algorithm] = [ + .aes128GCM, .aes192GCM, .aes256GCM, .chaChaPoly + ] + + func test_givenEncryptedData_whenDeobfuscate_thenReturnsDecryptedData() throws { + // given + let encryptedData = encryptionAlgorithms + .map(makeEncryptedData(using:)) + + // when + let decryptedData = try encryptedData + .map { algorithm, data in + try DataCrypter(algorithm: algorithm).deobfuscate(data) + } + + // then + decryptedData.forEach { XCTAssertEqual(plainData, $0) } + } +} + +extension DataCrypterTests { + + private func makeEncryptedData(using algorithm: Algorithm) -> (Algorithm, Data) { + let key = SymmetricKey(size: algorithm.keySize) + + var encryptedData: Data + switch algorithm { + case .aes128GCM, .aes192GCM, .aes256GCM: + encryptedData = try! AES.GCM.seal(plainData, using: key, nonce: .init()).combined! + case .chaChaPoly: + encryptedData = try! ChaChaPoly.seal(plainData, using: key, nonce: .init()).combined + } + + let keyData = key.withUnsafeBytes(Data.init(_:)) + encryptedData.append(keyData) + + return (algorithm, encryptedData) + } +} diff --git a/Tests/ConfidentialKitTests/Obfuscation/Techniques/Encryption/SymmetricEncryptionAlgorithmTests.swift b/Tests/ConfidentialKitTests/Obfuscation/Techniques/Encryption/SymmetricEncryptionAlgorithmTests.swift new file mode 100644 index 0000000..5c889da --- /dev/null +++ b/Tests/ConfidentialKitTests/Obfuscation/Techniques/Encryption/SymmetricEncryptionAlgorithmTests.swift @@ -0,0 +1,14 @@ +@testable import ConfidentialKit +import XCTest + +final class SymmetricEncryptionAlgorithmTests: XCTestCase { + + private typealias Algorithm = Obfuscation.Encryption.SymmetricEncryptionAlgorithm + + func test_whenKeySizeBitCount_thenReturnsExpectedNumberOfBits() { + XCTAssertEqual(Algorithm.aes128GCM.keySize.bitCount, 128) + XCTAssertEqual(Algorithm.aes192GCM.keySize.bitCount, 192) + XCTAssertEqual(Algorithm.aes256GCM.keySize.bitCount, 256) + XCTAssertEqual(Algorithm.chaChaPoly.keySize.bitCount, 256) + } +} diff --git a/Tests/ConfidentialKitTests/Obfuscation/Techniques/Randomization/DataShufflerTests.swift b/Tests/ConfidentialKitTests/Obfuscation/Techniques/Randomization/DataShufflerTests.swift new file mode 100644 index 0000000..620abf4 --- /dev/null +++ b/Tests/ConfidentialKitTests/Obfuscation/Techniques/Randomization/DataShufflerTests.swift @@ -0,0 +1,77 @@ +@testable import ConfidentialKit +import XCTest + +final class DataShufflerTests: XCTestCase { + + private typealias DataShuffler = Obfuscation.Randomization.DataShuffler + + private let nonce: UInt64 = 4213634601671549038 + + private var sut: DataShuffler! + + override func setUp() { + super.setUp() + sut = .init(nonce: nonce) + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + func test_givenShuffledData_whenDeobfuscate_thenReturnsDeshuffledData() throws { + // given + let plainData: [Data] = [ + .init([0x5d, 0x9b, 0xe7, 0xde]), + .init([0xe4, 0xe3, 0x34, 0xd8]), + .init([0x55, 0x2a, 0x49, 0x30]) + ] + let count = Int(4 ^ nonce) + let shuffledIndexes = [3,0,2,1] + let shuffledData = plainData.enumerated().map { idx, data -> Data in + let dataBytes = reorderBytes(.init(data), given: shuffledIndexes) + let indexesByteWidth = UInt8(1 << idx) + let obfuscatedIndexes = obfuscateIndexes( + shuffledIndexes, + byteWidth: indexesByteWidth + ) + return Data( + count.bytes + dataBytes + [indexesByteWidth] + obfuscatedIndexes + ) + } + + // when + let deshuffledData = try shuffledData.map { try sut.deobfuscate($0) } + + // then + XCTAssertEqual(plainData, deshuffledData) + } +} + +private extension DataShufflerTests { + + func reorderBytes(_ bytes: [UInt8], given indexes: [Int]) -> [UInt8] { + var result: [UInt8] = .init(repeating: .zero, count: bytes.count) + indexes.enumerated().forEach { oldIdx, newIdx in + result[newIdx] = bytes[oldIdx] + } + + return result + } + + func obfuscateIndexes(_ indexes: [Int], byteWidth: UInt8) -> [UInt8] { + switch Int(byteWidth) { + case UInt8.byteWidth: + return indexes + .map(UInt8.init) + .map { $0 ^ .init(bytes: nonce.bytes) } + case UInt16.byteWidth: + return indexes + .map(UInt16.init) + .flatMap { withUnsafeBytes(of: $0 ^ .init(bytes: nonce.bytes), [UInt8].init) } + default: + return indexes + .flatMap { withUnsafeBytes(of: $0 ^ .init(bytes: nonce.bytes), [UInt8].init) } + } + } +} diff --git a/Tests/ConfidentialKitTests/TestDoubles/Spies/DataDecoderSpy.swift b/Tests/ConfidentialKitTests/TestDoubles/Spies/DataDecoderSpy.swift new file mode 100644 index 0000000..49c9ba3 --- /dev/null +++ b/Tests/ConfidentialKitTests/TestDoubles/Spies/DataDecoderSpy.swift @@ -0,0 +1,18 @@ +@testable import ConfidentialKit +import Foundation + +final class DataDecoderSpy: DataDecoder { + + var underlyingDecoder: DataDecoder + + private(set) var decodeRecordedData: [Data] = [] + + init(underlyingDecoder: DataDecoder) { + self.underlyingDecoder = underlyingDecoder + } + + func decode(_ type: D.Type, from data: Data) throws -> D { + decodeRecordedData.append(data) + return try underlyingDecoder.decode(type, from: data) + } +} diff --git a/Tests/ConfidentialKitTests/TestDoubles/Spies/DeobfuscateDataFuncSpy.swift b/Tests/ConfidentialKitTests/TestDoubles/Spies/DeobfuscateDataFuncSpy.swift new file mode 100644 index 0000000..cae0763 --- /dev/null +++ b/Tests/ConfidentialKitTests/TestDoubles/Spies/DeobfuscateDataFuncSpy.swift @@ -0,0 +1,11 @@ +import Foundation + +final class DeobfuscateDataFuncSpy { + + private(set) var recordedData: [Data] = [] + + func deobfuscateData(_ data: Data) -> Data { + recordedData.append(data) + return data + } +} diff --git a/Tests/ConfidentialKitTests/TestDoubles/Stubs/SecretStubFactory.swift b/Tests/ConfidentialKitTests/TestDoubles/Stubs/SecretStubFactory.swift new file mode 100644 index 0000000..521495e --- /dev/null +++ b/Tests/ConfidentialKitTests/TestDoubles/Stubs/SecretStubFactory.swift @@ -0,0 +1,12 @@ +import ConfidentialKit +import Foundation + +extension Obfuscation.Secret { + + enum StubFactory { + + static func makeJSONEncodedSecret(with singleValue: SingleValue) -> Obfuscation.Secret { + .init(data: try! JSONEncoder().encode(singleValue).map { $0 }) + } + } +} diff --git a/Tests/ConfidentialKitTests/TestDoubles/Stubs/SingleValueStubFactory.swift b/Tests/ConfidentialKitTests/TestDoubles/Stubs/SingleValueStubFactory.swift new file mode 100644 index 0000000..cb34404 --- /dev/null +++ b/Tests/ConfidentialKitTests/TestDoubles/Stubs/SingleValueStubFactory.swift @@ -0,0 +1,18 @@ +import ConfidentialKit +import Foundation + +typealias SingleValue = Obfuscation.SupportedDataTypes.SingleValue + +extension SingleValue { + + enum StubFactory { + + static func makeSecretMessage() -> SingleValue { + "Secret message ๐Ÿ”" + } + + static func makeSecretMessageData() -> Data { + makeSecretMessage().data(using: .utf8)! + } + } +} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..d5ed7ff --- /dev/null +++ b/codecov.yml @@ -0,0 +1,9 @@ +coverage: + status: + project: + default: + target: 90% + threshold: 0.25% + patch: + default: + informational: true \ No newline at end of file