├── .github └── workflows │ ├── codeql.yml │ └── swift.yml ├── .gitignore ├── .spi.yml ├── .swiftformat ├── CLAUDE.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── LICENSE ├── Makefile ├── Package.swift ├── Package@swift-6.0.swift ├── Package@swift-6.1.swift ├── README.md ├── SECURITY.md ├── Sources ├── Czlib │ ├── module.modulemap │ ├── shim.c │ └── shim.h └── JWSETKit │ ├── Base │ ├── EncryptedData.swift │ ├── Error.swift │ ├── Expiry.swift │ ├── ProtectedContainer.swift │ ├── RawType.swift │ ├── Storage.swift │ ├── StorageField.swift │ └── WebContainer.swift │ ├── Cryptography │ ├── Algorithms │ │ ├── Algorithms.swift │ │ ├── Compression.swift │ │ ├── ContentEncryption.swift │ │ ├── KeyEncryption.swift │ │ ├── RFC5480Algorithms.swift │ │ └── Signature.swift │ ├── Certificate │ │ ├── JSONWebCertificate.swift │ │ ├── SecCertificate.swift │ │ └── X509Certificate.swift │ ├── Compression │ │ ├── Apple.swift │ │ └── Zlib.swift │ ├── EC │ │ ├── ECCAbstract.swift │ │ ├── Ed25519.swift │ │ ├── HPKE.swift │ │ ├── JWK-EC.swift │ │ ├── P256.swift │ │ ├── P384.swift │ │ └── P521.swift │ ├── Hashing │ │ └── SHA.swift │ ├── KeyASN1.swift │ ├── KeyAccessors.swift │ ├── KeyExporter.swift │ ├── KeyParser.swift │ ├── KeySet.swift │ ├── Keys.swift │ ├── PQC │ │ ├── JWK-MLDSA.swift │ │ ├── MLDSA65.swift │ │ ├── MLDSA87.swift │ │ └── ModuleLatticeAbstract.swift │ ├── RSA │ │ ├── JWK-RSA.swift │ │ ├── RSA.swift │ │ ├── RSA_boring.swift │ │ └── SecKey.swift │ └── Symmetric │ │ ├── AES-CBC-HMAC.swift │ │ ├── AES.swift │ │ ├── CommonCrypto.swift │ │ ├── ConcatKDF.swift │ │ ├── Direct.swift │ │ ├── HMAC.swift │ │ ├── PBKDF2.swift │ │ └── SymmetricKey.swift │ ├── Documentation.docc │ ├── Extensions │ │ ├── JWA.md │ │ ├── JWE.md │ │ ├── JWK.md │ │ ├── JWS.md │ │ ├── JWSETKit.md │ │ └── JWT.md │ └── Manuals │ │ ├── 3-Cryptography.md │ │ ├── 5-SecurityGuidelines.md │ │ └── 7-Extending-Container.md │ ├── Entities │ ├── JOSE │ │ ├── JOSE-JWEHPKERegistered.swift │ │ ├── JOSE-JWERegistered.swift │ │ ├── JOSE-JWSRegistered.swift │ │ └── JOSEHeader.swift │ ├── JWE │ │ ├── JWE.swift │ │ ├── JWECodable.swift │ │ ├── JWEHeader.swift │ │ └── JWERecipient.swift │ ├── JWS │ │ ├── JWS.swift │ │ ├── JWSCodable.swift │ │ └── JWSHeader.swift │ └── JWT │ │ ├── JWTOAuthClaims.swift │ │ ├── JWTOIDCAuth.swift │ │ ├── JWTOIDCStandard.swift │ │ ├── JWTPayload.swift │ │ ├── JWTPopClaims.swift │ │ └── JWTRegisteredClaims.swift │ ├── Extensions │ ├── ASN1.swift │ ├── Base64.swift │ ├── Codable.swift │ ├── Data.swift │ ├── KeyLookup.swift │ ├── Localizing.swift │ └── LockValue.swift │ ├── PrivacyInfo.xcprivacy │ └── Resources │ ├── ar.lproj │ └── Localizable.stringsdict │ ├── en.lproj │ └── Localizable.stringsdict │ ├── es.lproj │ └── Localizable.stringsdict │ ├── fa.lproj │ └── Localizable.stringsdict │ └── fr.lproj │ └── Localizable.stringsdict └── Tests └── JWSETKitTests ├── Base ├── StorageTests.swift └── WebContainerTests.swift ├── Cryptography ├── CompressionTests.swift ├── ECTests.swift ├── JWKSetTests.swift ├── MLDSATests.swift ├── RFC7520DecryptionTests.swift ├── RFC7520EncryptionTests.swift ├── RFC7520SignatureTests.swift ├── RSACryptoTests.swift ├── RSATests.swift └── ThumbprintTests.swift ├── Entities ├── JOSEHeaderJWETests.swift ├── JOSEHeaderJWSTests.swift ├── JWETests.swift ├── JWSMLDSATests.swift ├── JWSTests.swift ├── JWTOAuthClaimsTests.swift ├── JWTOIDCAuthClaimsTests.swift ├── JWTOIDCStandardClaimsTests.swift ├── JWTPopClaimsTests.swift ├── JWTRegisteredClaimsTests.swift └── JWTTests.swift ├── ExampleKeys.swift └── Extensions ├── Base64Tests.swift ├── KeyLookupTests.swift └── LocalizingTests.swift /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '16 4 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | # Runner size impacts CodeQL analysis time. To learn more, please see: 27 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 28 | # - https://gh.io/supported-runners-and-hardware-resources 29 | # - https://gh.io/using-larger-runners 30 | # Consider using larger runners for possible analysis time improvements. 31 | runs-on: 'macos-latest' 32 | permissions: 33 | actions: read 34 | contents: read 35 | security-events: write 36 | 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | language: [ 'swift' ] 41 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby', 'swift' ] 42 | # Use only 'java' to analyze code written in Java, Kotlin or both 43 | # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both 44 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 45 | 46 | steps: 47 | - name: Checkout repository 48 | uses: actions/checkout@v4 49 | 50 | - name: Select Xcode version 51 | run: sudo xcode-select -s /Applications/Xcode_16.1.app/Contents/Developer 52 | 53 | # Initializes the CodeQL tools for scanning. 54 | - name: Initialize CodeQL 55 | uses: github/codeql-action/init@v3 56 | with: 57 | languages: ${{ matrix.language }} 58 | # If you wish to specify custom queries, you can do so here or in a config file. 59 | # By default, queries listed here will override any specified in a config file. 60 | # Prefix the list here with "+" to use these queries and those in the config file. 61 | 62 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 63 | # queries: security-extended,security-and-quality 64 | 65 | 66 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 67 | # If this step fails, then you should remove it and run the build manually (see below) 68 | - name: Autobuild 69 | uses: github/codeql-action/autobuild@v3 70 | 71 | # ℹ️ Command-line programs to run using the OS shell. 72 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 73 | 74 | # If the Autobuild fails above, remove it and uncomment the following three lines. 75 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 76 | 77 | # - run: | 78 | # echo "Run, Build Application using script" 79 | # ./location_of_script_within_repo/buildscript.sh 80 | 81 | - name: Perform CodeQL Analysis 82 | uses: github/codeql-action/analyze@v3 83 | with: 84 | category: "/language:${{matrix.language}}" 85 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build-and-test: 14 | name: Swift ${{ matrix.swift }} on ${{ matrix.os }} 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [macos-14, ubuntu-latest] 19 | swift: ["6.1", "6.0", "5.10"] 20 | 21 | steps: 22 | - uses: SwiftyLab/setup-swift@latest 23 | with: 24 | swift-version: ${{ matrix.swift }} 25 | - uses: actions/checkout@v4 26 | - name: Get swift version 27 | run: swift --version 28 | - name: Build 29 | run: swift build -v 30 | - name: Run tests 31 | run: swift test -v --enable-code-coverage 32 | - name: Submit code coverage 33 | uses: vapor/swift-codecov-action@v0.3 34 | with: 35 | codecov_token: ${{ secrets.CODECOV_TOKEN }} 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,swiftpm,swiftpackagemanager 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift,swiftpm,swiftpackagemanager 3 | 4 | ### Swift ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 43 | Packages/ 44 | Package.pins 45 | Package.resolved 46 | # *.xcodeproj 47 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 48 | # hence it is not needed unless you have added a package configuration file to your project 49 | .swiftpm 50 | 51 | .build/ 52 | 53 | # CocoaPods 54 | # We recommend against adding the Pods directory to your .gitignore. However 55 | # you should judge for yourself, the pros and cons are mentioned at: 56 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 57 | # Pods/ 58 | # Add this line if you want to avoid checking in source code from the Xcode workspace 59 | # *.xcworkspace 60 | 61 | # Carthage 62 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 63 | Carthage/Checkouts 64 | 65 | Carthage/Build/ 66 | 67 | # Accio dependency management 68 | Dependencies/ 69 | .accio/ 70 | 71 | # fastlane 72 | # It is recommended to not store the screenshots in the git repo. 73 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 74 | # For more information about the recommended setup visit: 75 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 76 | 77 | fastlane/report.xml 78 | fastlane/Preview.html 79 | fastlane/screenshots/**/*.png 80 | fastlane/test_output 81 | 82 | # Code Injection 83 | # After new code Injection tools there's a generated folder /iOSInjectionProject 84 | # https://github.com/johnno1962/injectionforxcode 85 | 86 | iOSInjectionProject/ 87 | 88 | ### SwiftPackageManager ### 89 | Packages 90 | xcuserdata 91 | *.xcodeproj 92 | 93 | 94 | ### SwiftPM ### 95 | 96 | 97 | ### Xcode ### 98 | 99 | ## Xcode 8 and earlier 100 | 101 | ### Xcode Patch ### 102 | *.xcodeproj/* 103 | !*.xcodeproj/project.pbxproj 104 | !*.xcodeproj/xcshareddata/ 105 | !*.xcodeproj/project.xcworkspace/ 106 | !*.xcworkspace/contents.xcworkspacedata 107 | /*.gcno 108 | **/xcshareddata/WorkspaceSettings.xcsettings 109 | 110 | # End of https://www.toptal.com/developers/gitignore/api/xcode,swift,swiftpm,swiftpackagemanager 111 | 112 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: ios 5 | scheme: JWSETKit 6 | - platform: macos-xcodebuild 7 | scheme: JWSETKit 8 | - platform: tvos 9 | scheme: JWSETKit 10 | - documentation_targets: [JWSETKit] 11 | -------------------------------------------------------------------------------- /.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --exclude Snapshots,Build 4 | 5 | # format options 6 | --acronyms 7 | --allman false 8 | --binarygrouping 4,8 9 | --commas always 10 | --decimalgrouping 3,6 11 | --elseposition same-line 12 | --voidtype void 13 | --exponentcase lowercase 14 | --exponentgrouping disabled 15 | --extensionacl on-declarations 16 | --fractiongrouping disabled 17 | --header ignore 18 | --hexgrouping 4,8 19 | --hexliteralcase uppercase 20 | --ifdef outdent 21 | --indent 4 22 | --indentcase false 23 | --importgrouping testable-bottom 24 | --linebreaks lf 25 | --maxwidth none 26 | --octalgrouping 4,8 27 | --operatorfunc spaced 28 | --patternlet inline 29 | --ranges spaced 30 | --self init-only 31 | --semicolons inline 32 | --stripunusedargs always 33 | --swiftversion 5.1 34 | --trimwhitespace nonblank-lines 35 | --wraparguments preserve 36 | --wrapcollections preserve 37 | 38 | # rules 39 | 40 | --enable isEmpty 41 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | JWSETKit is a Swift library for working with JSON Web Signature (JWS), JSON Web Encryption (JWE), and JSON Web Token (JWT) according to JOSE (JSON Object Signing and Encryption) standards. The library provides comprehensive cryptographic functionality for signing, encrypting, and verifying JSON web tokens with support for multiple algorithms and key types. 8 | 9 | ## Development Commands 10 | 11 | ### Building and Testing 12 | ```bash 13 | # Build the project 14 | swift build -v 15 | 16 | # Run all tests 17 | swift test -v 18 | 19 | # Run tests with code coverage 20 | swift test -v --enable-code-coverage 21 | 22 | # Run tests using Make 23 | make test 24 | ``` 25 | 26 | ### Cross-Platform Testing 27 | ```bash 28 | # Test on Linux using Docker 29 | make linuxtest 30 | 31 | # Clean Linux test (builds fresh container) 32 | make cleanlinuxtest 33 | ``` 34 | 35 | ### Package Management 36 | The project uses Swift Package Manager with dependencies defined in `Package.swift`. Key dependencies: 37 | - `swift-collections` for data structures 38 | - `swift-asn1` for ASN.1 parsing 39 | - `swift-crypto` for cryptographic operations 40 | - `swift-certificates` for X.509 certificate support 41 | - `swift-testing` for testing framework 42 | 43 | ## Architecture Overview 44 | 45 | ### Core Components 46 | 47 | **Base Layer** (`Sources/JWSETKit/Base/`): 48 | - `WebContainer.swift`: Core protocol for JSON containers used throughout JOSE structures 49 | - `ProtectedContainer.swift`: Base for protected (signed/encrypted) containers 50 | - `Storage.swift`: Key-value storage abstraction for JOSE headers and payloads 51 | - `Error.swift`: Centralized error handling for the library 52 | 53 | **Entities** (`Sources/JWSETKit/Entities/`): 54 | - **JWS** (`JWS/`): JSON Web Signature implementation with support for multiple signatures 55 | - **JWE** (`JWE/`): JSON Web Encryption with recipient-based encryption 56 | - **JWT** (`JWT/`): JSON Web Token built on top of JWS with registered claims 57 | - **JOSE** (`JOSE/`): JOSE header implementations for different token types 58 | 59 | **Cryptography** (`Sources/JWSETKit/Cryptography/`): 60 | - **Algorithms** (`Algorithms/`): Algorithm abstractions and implementations for signature, key encryption, and content encryption 61 | - **EC** (`EC/`): Elliptic curve cryptography (P-256, P-384, P-521, Ed25519, HPKE) 62 | - **RSA** (`RSA/`): RSA cryptography with PKCS#1 and PSS padding 63 | - **PQC** (`PQC/`): Post-quantum cryptography (ML-DSA variants) 64 | - **Symmetric** (`Symmetric/`): Symmetric key operations (AES, HMAC, KDF) 65 | - **Certificate** (`Certificate/`): X.509 certificate handling and JSON Web Certificate support 66 | 67 | ### Key Design Patterns 68 | 69 | **Generic Containers**: The library uses generic `JSONWebSignature` and similar structures where `Payload` conforms to `ProtectedWebContainer`. This allows type-safe handling of different payload types (JWT claims, arbitrary JSON, etc.). 70 | 71 | **Algorithm Abstraction**: Cryptographic algorithms implement the `JSONWebAlgorithm` protocol, providing consistent interfaces for signature, key encryption, and content encryption algorithms. 72 | 73 | **Storage Pattern**: JSON data is managed through `JSONWebValueStorage` which provides dynamic member lookup and type-safe access to JOSE header fields and JWT claims. 74 | 75 | **Cross-Platform Support**: The library uses conditional imports (`FoundationEssentials` vs `Foundation`) and platform-specific implementations to support both Apple platforms and Linux. 76 | 77 | ## Key Types and Protocols 78 | 79 | - `JSONWebContainer`: Base protocol for all JOSE JSON structures 80 | - `ProtectedWebContainer`: Protocol for containers that can be protected (signed/encrypted) 81 | - `JSONWebAlgorithm`: Protocol for cryptographic algorithms 82 | - `JSONWebSignature`: Generic JWS structure 83 | - `JSONWebEncryption`: JWE structure with recipient support 84 | - `JSONWebToken`: Type alias for JWS with JWT claims payload 85 | 86 | ## Supported Algorithms 87 | 88 | The library implements extensive algorithm support including: 89 | - **Signatures**: HS256/384/512, RS256/384/512, ES256/384/512, PS256/384/512, EdDSA, ML-DSA-65/87 90 | - **Key Encryption**: RSA1_5, RSA-OAEP(-256,-384,-512), A128/192/256KW, ECDH-ES variants, A128/192/256GCMKW, PBES2 91 | - **Content Encryption**: A128/192/256GCM, A128/192/256CBC-HS256/384/512 92 | 93 | ## Testing Structure 94 | 95 | Tests are organized in `Tests/JWSETKitTests/` with: 96 | - **Base/**: Tests for core container and storage functionality 97 | - **Cryptography/**: Algorithm-specific tests and RFC compliance tests 98 | - **Entities/**: Tests for JWS, JWE, JWT structures 99 | - **Extensions/**: Tests for utility extensions 100 | 101 | The test suite includes RFC 7520 compliance tests for encryption, decryption, and signature verification. 102 | 103 | ## Platform Support 104 | 105 | - **Apple Platforms**: iOS 14+, macOS 11+, tvOS 14+, macCatalyst 14+ 106 | - **Linux**: Supported via Docker testing 107 | - **Swift Versions**: 5.10, 6.0, 6.1 108 | 109 | ## Notes for Development 110 | 111 | - X.509 certificate support is optional from Swift 6.1+ and requires explicit trait activation 112 | - The library supports both compact (base64url) and JSON serialization formats 113 | - Post-quantum cryptography (ML-DSA) is supported on macOS/iOS 26+ 114 | - Cross-platform differences are handled through conditional compilation and traits -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The code of conduct for this project can be found at https://swift.org/code-of-conduct. 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM swift:6.1 2 | 3 | # Add nonroot user 4 | RUN groupadd -r nonroot && useradd -r -g nonroot nonroot \ 5 | && mkdir -p /home/nonroot \ 6 | && chown -R nonroot:nonroot /home/nonroot \ 7 | && mkdir -p /home/nonroot/src/app \ 8 | && chown -R nonroot:nonroot /home/nonroot/src/app 9 | USER nonroot 10 | 11 | # Change working dir to /usr/src/app 12 | WORKDIR /home/nonroot/src/app 13 | VOLUME /home/nonroot/src/app 14 | 15 | COPY Sources ./Sources 16 | COPY Tests ./Tests 17 | COPY Package.swift ./ 18 | ENTRYPOINT ["swift", "test"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Amir Abbas Mousavian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | swift test 3 | 4 | linuxtest: 5 | docker build -f Dockerfile -t linuxtest . 6 | docker run --rm -v .:/home/nonroot/src/app linuxtest 7 | 8 | cleanlinuxtest: 9 | docker build -f Dockerfile -t linuxtest . 10 | docker run --rm linuxtest 11 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | extension [Platform] { 7 | static let darwin: [Platform] = [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS] 8 | static let nonWasm: [Platform] = [.linux, .windows, .android, .openbsd] 9 | static let nonDarwin: [Platform] = nonWasm + [.wasi] 10 | } 11 | 12 | let package = Package( 13 | name: "JWSETKit", 14 | defaultLocalization: "en", 15 | platforms: [ 16 | .iOS(.v14), 17 | .macOS(.v11), 18 | .tvOS(.v14), 19 | .macCatalyst(.v14), 20 | ], 21 | products: [ 22 | .library( 23 | name: "JWSETKit", 24 | targets: ["JWSETKit"] 25 | ), 26 | ], 27 | dependencies: [ 28 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"), 29 | .package(url: "https://github.com/apple/swift-asn1.git", from: "1.4.0"), 30 | .package(url: "https://github.com/apple/swift-crypto.git", from: "3.15.0"), 31 | .package(url: "https://github.com/apple/swift-certificates", from: "1.13.0"), 32 | .package(url: "https://github.com/swiftlang/swift-testing.git", exact: "0.10.0"), 33 | ], 34 | targets: [ 35 | .systemLibrary( 36 | name: "Czlib", 37 | pkgConfig: "zlib", 38 | providers: [ 39 | .apt(["zlib1g-dev"]), 40 | .brew(["zlib"]), 41 | .yum(["zlib-devel"]), 42 | ] 43 | ), 44 | .target( 45 | name: "JWSETKit", 46 | dependencies: [ 47 | .product(name: "Collections", package: "swift-collections"), 48 | .product(name: "SwiftASN1", package: "swift-asn1"), 49 | .product(name: "Crypto", package: "swift-crypto"), 50 | .product(name: "X509", package: "swift-certificates"), 51 | // Linux support 52 | .product(name: "_CryptoExtras", package: "swift-crypto", condition: .when(platforms: .nonDarwin)), 53 | .byName(name: "Czlib", condition: .when(platforms: .nonDarwin)), 54 | ], 55 | resources: [ 56 | .process("PrivacyInfo.xcprivacy"), 57 | ] 58 | ), 59 | .testTarget( 60 | name: "JWSETKitTests", 61 | dependencies: [ 62 | "JWSETKit", 63 | .product(name: "Testing", package: "swift-testing"), 64 | ] 65 | ), 66 | ] 67 | ) 68 | -------------------------------------------------------------------------------- /Package@swift-6.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | extension [Platform] { 7 | static let darwin: [Platform] = [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS] 8 | static let nonWasm: [Platform] = [.linux, .windows, .android, .openbsd] 9 | static let nonDarwin: [Platform] = nonWasm + [.wasi] 10 | } 11 | 12 | let package = Package( 13 | name: "JWSETKit", 14 | defaultLocalization: "en", 15 | platforms: [ 16 | .iOS(.v14), 17 | .macOS(.v11), 18 | .tvOS(.v14), 19 | .macCatalyst(.v14), 20 | ], 21 | products: [ 22 | .library( 23 | name: "JWSETKit", 24 | targets: ["JWSETKit"] 25 | ), 26 | ], 27 | dependencies: [ 28 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"), 29 | .package(url: "https://github.com/apple/swift-asn1.git", from: "1.4.0"), 30 | .package(url: "https://github.com/apple/swift-crypto.git", from: "3.15.0"), 31 | .package(url: "https://github.com/apple/swift-certificates", from: "1.13.0"), 32 | ], 33 | targets: [ 34 | .systemLibrary( 35 | name: "Czlib", 36 | pkgConfig: "zlib", 37 | providers: [ 38 | .apt(["zlib1g-dev"]), 39 | .brew(["zlib"]), 40 | .yum(["zlib-devel"]), 41 | ] 42 | ), 43 | .target( 44 | name: "JWSETKit", 45 | dependencies: [ 46 | .product(name: "Collections", package: "swift-collections"), 47 | .product(name: "SwiftASN1", package: "swift-asn1"), 48 | .product(name: "Crypto", package: "swift-crypto"), 49 | .product(name: "X509", package: "swift-certificates", condition: .when(platforms: .darwin + .nonWasm)), 50 | // Linux support 51 | .product(name: "_CryptoExtras", package: "swift-crypto", condition: .when(platforms: .nonDarwin)), 52 | .byName(name: "Czlib", condition: .when(platforms: .nonWasm)), 53 | ], 54 | resources: [ 55 | .process("PrivacyInfo.xcprivacy"), 56 | ], 57 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")] 58 | ), 59 | .testTarget( 60 | name: "JWSETKitTests", 61 | dependencies: ["JWSETKit"] 62 | ), 63 | ] 64 | ) 65 | -------------------------------------------------------------------------------- /Package@swift-6.1.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | extension [Platform] { 7 | static let darwin: [Platform] = [.macOS, .macCatalyst, .iOS, .tvOS, .watchOS, .visionOS] 8 | static let nonWasm: [Platform] = [.linux, .windows, .android, .openbsd] 9 | static let nonDarwin: [Platform] = nonWasm + [.wasi] 10 | } 11 | 12 | let package = Package( 13 | name: "JWSETKit", 14 | defaultLocalization: "en", 15 | platforms: [ 16 | .iOS(.v14), 17 | .macOS(.v11), 18 | .tvOS(.v14), 19 | .macCatalyst(.v14), 20 | ], 21 | products: [ 22 | .library( 23 | name: "JWSETKit", 24 | targets: ["JWSETKit"] 25 | ), 26 | ], 27 | traits: [ 28 | "X509", 29 | .default(enabledTraits: []), 30 | ], 31 | dependencies: [ 32 | .package(url: "https://github.com/apple/swift-collections.git", from: "1.2.0"), 33 | .package(url: "https://github.com/apple/swift-asn1.git", from: "1.4.0"), 34 | .package(url: "https://github.com/apple/swift-crypto.git", from: "3.15.0"), 35 | .package(url: "https://github.com/apple/swift-certificates", from: "1.13.0"), 36 | ], 37 | targets: [ 38 | .systemLibrary( 39 | name: "Czlib", 40 | pkgConfig: "zlib", 41 | providers: [ 42 | .apt(["zlib1g-dev"]), 43 | .brew(["zlib"]), 44 | .yum(["zlib-devel"]), 45 | ] 46 | ), 47 | .target( 48 | name: "JWSETKit", 49 | dependencies: [ 50 | .product(name: "Collections", package: "swift-collections"), 51 | .product(name: "SwiftASN1", package: "swift-asn1"), 52 | .product(name: "Crypto", package: "swift-crypto"), 53 | .product(name: "X509", package: "swift-certificates", condition: .when(platforms: .darwin + .nonWasm, traits: ["X509"])), 54 | // Linux support 55 | .product(name: "_CryptoExtras", package: "swift-crypto", condition: .when(platforms: .nonDarwin)), 56 | .byName(name: "Czlib", condition: .when(platforms: .nonWasm)), 57 | ], 58 | resources: [ 59 | .process("PrivacyInfo.xcprivacy"), 60 | ], 61 | swiftSettings: [.enableUpcomingFeature("ExistentialAny")] 62 | ), 63 | .testTarget( 64 | name: "JWSETKitTests", 65 | dependencies: ["JWSETKit"] 66 | ), 67 | ] 68 | ) 69 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This document specifies the security process for JWSETKit. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | To report a known or suspected vulnerability in JWSETKit, 8 | [use this section to report a vulnerability.](https://github.com/amosavian/JWSETKit/security/advisories/new) 9 | 10 | Fixes to JWSETKit will be released simultaneously with any changes that need to be made in JWSETKit, 11 | to avoid the risk of having only partially fixed projects. 12 | -------------------------------------------------------------------------------- /Sources/Czlib/module.modulemap: -------------------------------------------------------------------------------- 1 | module Czlib { 2 | header "shim.h" 3 | link "z" 4 | export * 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Czlib/shim.c: -------------------------------------------------------------------------------- 1 | // 2 | // shim.c 3 | // JWSETKit 4 | // 5 | // Created by Amir Abbas Mousavian on 1/11/25. 6 | // 7 | 8 | #include "shim.h" 9 | -------------------------------------------------------------------------------- /Sources/Czlib/shim.h: -------------------------------------------------------------------------------- 1 | // 2 | // shim.h 3 | // JWSETKit 4 | // 5 | // Created by Amir Abbas Mousavian on 1/11/25. 6 | // 7 | 8 | #ifndef CZLIB_SHIM 9 | #define CZLIB_SHIM 10 | 11 | #include 12 | 13 | #endif /* CZLIB_SHIM */ 14 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Base/EncryptedData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EncryptedData.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/7/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | /// A container for AES ciphers, e.g. AES-GCM, AES-CBC-HMAC, etc. 16 | @frozen 17 | public struct SealedData: DataProtocol, BidirectionalCollection, Hashable, Sendable { 18 | public typealias Nonce = Data 19 | 20 | /// The nonce used to encrypt the data. 21 | public let nonce: Nonce 22 | 23 | /// The encrypted data. 24 | public let ciphertext: Data 25 | 26 | /// An authentication tag. 27 | public let tag: Data 28 | 29 | public var regions: [Data] { 30 | [nonce, ciphertext, tag].map { $0 } 31 | } 32 | 33 | /// A combined element composed of the nonce, encrypted data, and authentication tag. 34 | public var combined: Data { 35 | Data(regions.joined()) 36 | } 37 | 38 | public var startIndex: Int { 39 | 0 40 | } 41 | 42 | public var endIndex: Int { 43 | nonce.count + ciphertext.count + tag.count 44 | } 45 | 46 | public subscript(position: Int) -> UInt8 { 47 | if position < nonce.count { 48 | return nonce[position] 49 | } else if position < nonce.count + ciphertext.count { 50 | return ciphertext[position - nonce.count] 51 | } else { 52 | return tag[position] 53 | } 54 | } 55 | 56 | public subscript(bounds: Range) -> Data { 57 | combined[bounds] 58 | } 59 | 60 | /// Creates a sealed box from the given tag, nonce, and ciphertext. 61 | /// 62 | /// - Parameters: 63 | /// - nonce: The nonce or initial vector. 64 | /// - ciphertext: The encrypted data. 65 | /// - tag: The authentication tag. 66 | public init(nonce: Nonce, ciphertext: C, tag: T) { 67 | self.nonce = nonce 68 | self.ciphertext = .init(ciphertext) 69 | self.tag = .init(tag) 70 | } 71 | 72 | /// Creates a sealed box from the given AES sealed box. 73 | /// 74 | /// - Parameters: 75 | /// - sealedBox: Container for your data. 76 | public init(_ sealedBox: AES.GCM.SealedBox) { 77 | self.nonce = Data(sealedBox.nonce) 78 | self.ciphertext = sealedBox.ciphertext 79 | self.tag = sealedBox.tag 80 | } 81 | 82 | /// Creates a sealed box from the given ChaChaPoly sealed box. 83 | /// - Parameters: 84 | /// - sealedBox: Container for your data. 85 | public init(_ sealedBox: ChaChaPoly.SealedBox) { 86 | self.nonce = Data(sealedBox.nonce) 87 | self.ciphertext = sealedBox.ciphertext 88 | self.tag = sealedBox.tag 89 | } 90 | 91 | /// Creates a sealed box from the given AES sealed box. 92 | /// 93 | /// - Parameters: 94 | /// - sealedBox: Container for your data. 95 | public init(combined data: D, nonceLength: Int, tagLength: Int) throws where D: DataProtocol { 96 | guard data.count >= nonceLength + tagLength else { 97 | throw CryptoKitError.incorrectParameterSize 98 | } 99 | self.nonce = Data(data.prefix(nonceLength)) 100 | self.ciphertext = Data(data.dropFirst(nonceLength).dropLast(tagLength)) 101 | self.tag = Data(data.suffix(tagLength)) 102 | } 103 | 104 | public static func == (lhs: SealedData, rhs: SealedData) -> Bool { 105 | lhs.nonce == rhs.nonce && lhs.ciphertext == rhs.ciphertext && lhs.tag == rhs.tag 106 | } 107 | } 108 | 109 | extension AES.GCM.SealedBox { 110 | /// Creates a AES sealed box from the given sealed box. 111 | /// 112 | /// - Parameters: 113 | /// - sealedBox: Container for your data. 114 | public init(_ sealedData: SealedData) throws { 115 | try self.init( 116 | nonce: .init(data: sealedData.nonce), 117 | ciphertext: sealedData.ciphertext, 118 | tag: sealedData.tag 119 | ) 120 | } 121 | } 122 | 123 | extension ChaChaPoly.SealedBox { 124 | /// Creates a ChaChaPoly sealed box from the given sealed box. 125 | /// 126 | /// - Parameters: 127 | /// - sealedBox: Container for your data. 128 | public init(_ sealedData: SealedData) throws { 129 | try self.init( 130 | nonce: .init(data: sealedData.nonce), 131 | ciphertext: sealedData.ciphertext, 132 | tag: sealedData.tag 133 | ) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Base/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/7/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | /// Error type thrown by JWSETKit framework. 15 | public protocol JSONWebError: LocalizedError { 16 | /// Localized error description in given locale's language. 17 | func localizedError(for locale: Locale) -> String 18 | } 19 | 20 | extension JSONWebError { 21 | public var errorDescription: String? { 22 | localizedError(for: .current) 23 | } 24 | } 25 | 26 | /// Errors occurred during key creation or usage. 27 | /// 28 | /// - Note: Localization of `errorDescription` can be changes by setting `jsonWebKeyLocale`. 29 | public enum JSONWebKeyError: JSONWebError, Sendable { 30 | /// Given algorithm is unsupported in the framework. 31 | case unknownAlgorithm 32 | 33 | /// Key type is not defined. 34 | /// 35 | /// Supported key types are `"EC"`, `"RSA"`, `"oct"`. 36 | case unknownKeyType 37 | 38 | /// Decipherment of given cipher-text has failed. 39 | case decryptionFailed 40 | 41 | /// Key not found. 42 | case keyNotFound 43 | 44 | /// Operation is not allowed with given class/struct or key. 45 | case operationNotAllowed 46 | 47 | /// Key format is invalid. 48 | case invalidKeyFormat 49 | 50 | /// A localized message describing what error occurred. 51 | public func localizedError(for locale: Locale) -> String { 52 | switch self { 53 | case .unknownAlgorithm: 54 | return .init( 55 | localizingKey: "errorUnknownAlgorithm", 56 | value: "Given signature/encryption algorithm is no supported.", 57 | locale: locale 58 | ) 59 | case .unknownKeyType: 60 | return .init( 61 | localizingKey: "errorUnknownKeyType", value: "Key type is not supported.", 62 | locale: locale 63 | ) 64 | case .decryptionFailed: 65 | return .init( 66 | localizingKey: "errorDecryptionFailed", 67 | value: "Decrypting cipher-text using given key is not possible.", 68 | locale: locale 69 | ) 70 | case .keyNotFound: 71 | return .init( 72 | localizingKey: "errorKeyNotFound", 73 | value: "Failed to find given key.", 74 | locale: locale 75 | ) 76 | case .operationNotAllowed: 77 | return .init( 78 | localizingKey: "errorOperationNotAllowed", 79 | value: "Operation Not Allowed.", 80 | locale: locale 81 | ) 82 | case .invalidKeyFormat: 83 | return .init( 84 | localizingKey: "errorInvalidKeyFormat", 85 | value: "Invalid Key Format", 86 | locale: locale 87 | ) 88 | } 89 | } 90 | } 91 | 92 | /// Validation errors including expired token. 93 | /// 94 | /// - Note: Localization of `errorDescription` can be changes by setting `jsonWebKeyLocale`. 95 | public enum JSONWebValidationError: JSONWebError, Sendable { 96 | /// Current date is after `"exp"` claim in token. 97 | case tokenExpired(expiry: Date) 98 | 99 | /// Current date is before `"nbf"` claim in token. 100 | case tokenInvalidBefore(notBefore: Date) 101 | 102 | /// Given audience is not enlisted in `"aud"` claim in token. 103 | case audienceNotIntended(String) 104 | 105 | /// A required field is missing. 106 | case missingRequiredField(key: String) 107 | 108 | private func formatDate(_ date: Date, locale: Locale) -> String { 109 | #if canImport(Foundation.NSDateFormatter) 110 | let dateFormatter = DateFormatter() 111 | dateFormatter.locale = locale 112 | dateFormatter.dateStyle = .medium 113 | dateFormatter.timeStyle = .medium 114 | return dateFormatter.string(from: date) 115 | #else 116 | return date.iso8601 117 | #endif 118 | } 119 | 120 | /// A localized message describing what error occurred. 121 | public func localizedError(for locale: Locale) -> String { 122 | switch self { 123 | case .tokenExpired(let date): 124 | return .init( 125 | localizingKey: "errorExpiredToken", 126 | value: "Token is invalid after %@", 127 | locale: locale, 128 | formatDate(date, locale: locale) 129 | ) 130 | case .tokenInvalidBefore(let date): 131 | return .init( 132 | localizingKey: "errorNotBeforeToken", 133 | value: "Token is invalid before %@", 134 | locale: locale, 135 | formatDate(date, locale: locale) 136 | ) 137 | case .audienceNotIntended(let audience): 138 | return .init( 139 | localizingKey: "errorInvalidAudience", 140 | value: "Audience \"%@\" is not intended for the token.", 141 | locale: locale, 142 | audience 143 | ) 144 | case .missingRequiredField(let key): 145 | return .init( 146 | localizingKey: "errorMissingField", 147 | value: "Required \"%@\" field is missing.", 148 | locale: locale, 149 | key 150 | ) 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Base/Expiry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Expiry.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/12/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | /// The container has a expire date or a starting "not before" date. 15 | public protocol Expirable { 16 | /// Verifies the current date/time is within the object start date and expiration date. 17 | /// 18 | /// - Parameter currentDate: current date/time that comparison takes against. 19 | func verifyDate(_ currentDate: Date) throws 20 | } 21 | 22 | extension Expirable { 23 | /// Verifies the current system date/time is within the object start date and expiration date. 24 | @inlinable 25 | func verifyDate() throws { 26 | try verifyDate(.init()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Base/RawType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RawType.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 11/24/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | /// Represents a type that can be initialized from a string raw value. 15 | public protocol StringRepresentable: RawRepresentable, Hashable, Codable, ExpressibleByStringLiteral, CustomStringConvertible, Sendable where StringLiteralType == String { 16 | init(rawValue: String) 17 | } 18 | 19 | extension StringRepresentable { 20 | public init(stringLiteral value: StringLiteralType) { 21 | self.init(rawValue: "\(value)") 22 | } 23 | 24 | public var description: String { 25 | rawValue 26 | } 27 | } 28 | 29 | #if swift(>=6) 30 | public typealias SendableAnyKeyPath = any AnyKeyPath & Sendable 31 | public typealias SendablePartialKeyPath = any PartialKeyPath & Sendable 32 | public typealias SendableKeyPath = any KeyPath & Sendable 33 | public typealias SendableWritableKeyPath = any Sendable & WritableKeyPath 34 | public typealias SendableReferenceWritableKeyPath = any ReferenceWritableKeyPath & Sendable 35 | #else 36 | public typealias SendableAnyKeyPath = AnyKeyPath 37 | public typealias SendablePartialKeyPath = PartialKeyPath 38 | public typealias SendableKeyPath = KeyPath 39 | public typealias SendableWritableKeyPath = WritableKeyPath 40 | public typealias SendableReferenceWritableKeyPath = ReferenceWritableKeyPath 41 | extension AnyKeyPath: @unchecked Sendable {} 42 | #endif 43 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Base/WebContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebContainer.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/7/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | /// JSON container for payloads and sections of JWS and JWE structures. 15 | @dynamicMemberLookup 16 | public protocol JSONWebContainer: Codable, Hashable { 17 | /// Storage of container values. 18 | var storage: JSONWebValueStorage { get } 19 | 20 | /// Returns a new concrete key using json data. 21 | /// 22 | /// - Parameter storage: Storage of key-values. 23 | init(storage: JSONWebValueStorage) throws 24 | 25 | /// Validates contents and required fields if applicable. 26 | func validate() throws 27 | } 28 | 29 | /// JSON container for payloads and sections of JWS and JWE structures. 30 | public protocol MutableJSONWebContainer: JSONWebContainer { 31 | /// Storage of container values. 32 | var storage: JSONWebValueStorage { get set } 33 | } 34 | 35 | @_documentation(visibility: private) 36 | public struct JSONWebContainerCustomParameters {} 37 | 38 | extension JSONWebContainer { 39 | /// Initializes container with filled data. 40 | /// 41 | /// - Parameter initializer: Setter of fields. 42 | public init(_ initializer: (_ container: inout Self) throws -> Void) throws { 43 | try self.init(storage: .init()) 44 | try initializer(&self) 45 | } 46 | 47 | public init(from decoder: any Decoder) throws { 48 | self = try Self(storage: .init()) 49 | let container = try decoder.singleValueContainer() 50 | try self.init(storage: container.decode(JSONWebValueStorage.self)) 51 | try validate() 52 | } 53 | 54 | public func encode(to encoder: any Encoder) throws { 55 | var container = encoder.singleValueContainer() 56 | try container.encode(storage) 57 | } 58 | 59 | public func validate() throws { 60 | // No validation is required by default. 61 | } 62 | 63 | /// Returns value of given key. 64 | public subscript(_ member: String) -> T? { 65 | storage[member] 66 | } 67 | 68 | @usableFromInline 69 | func stringKey(_ keyPath: SendableKeyPath) -> String { 70 | keyPath.name.jsonWebKey 71 | } 72 | 73 | @_documentation(visibility: private) 74 | @inlinable 75 | public subscript(dynamicMember member: SendableKeyPath) -> T? { 76 | storage[stringKey(member)] 77 | } 78 | 79 | @_documentation(visibility: private) 80 | @_disfavoredOverload 81 | @inlinable 82 | public subscript(dynamicMember member: String) -> T? { 83 | storage[member.jsonWebKey] 84 | } 85 | } 86 | 87 | extension JSONWebContainer where Self: CustomReflectable { 88 | public var customMirror: Mirror { 89 | storage.customMirror 90 | } 91 | } 92 | 93 | extension MutableJSONWebContainer { 94 | /// Returns value of given key. 95 | public subscript(_ member: String) -> T? { 96 | get { 97 | storage[member] 98 | } 99 | set { 100 | storage[member] = newValue 101 | } 102 | } 103 | 104 | @usableFromInline 105 | func stringKey(_ keyPath: SendableKeyPath) -> String { 106 | keyPath.name.jsonWebKey 107 | } 108 | 109 | @_documentation(visibility: private) 110 | @inlinable 111 | public subscript(dynamicMember member: SendableKeyPath) -> T? { 112 | get { 113 | storage[stringKey(member)] 114 | } 115 | set { 116 | storage[stringKey(member)] = newValue 117 | } 118 | } 119 | 120 | @_documentation(visibility: private) 121 | @_disfavoredOverload 122 | @inlinable 123 | public subscript(dynamicMember member: String) -> T? { 124 | get { 125 | storage[member.jsonWebKey] 126 | } 127 | set { 128 | storage[member.jsonWebKey] = newValue 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Algorithms/Compression.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Compression.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 10/13/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | #if canImport(Compression) 14 | import Compression 15 | #endif 16 | 17 | /// A protocol to provide compress/decompress data to support JWE content compression. 18 | public protocol JSONWebCompressor: Sendable { 19 | /// Compresses data using defined algorithm. 20 | /// 21 | /// - Parameter data: Data to be compressed. 22 | /// - Returns: Compressed data. 23 | static func compress(_ data: D) throws -> Data where D: DataProtocol 24 | 25 | /// Decompresses data using defined algorithm. 26 | /// 27 | /// - Parameter data: Data to be decompressed. 28 | /// - Returns: Decompressed data. 29 | static func decompress(_ data: D) throws -> Data where D: DataProtocol 30 | } 31 | 32 | /// Contains compression algorithm. 33 | public protocol CompressionCodec: Sendable { 34 | /// Compression algorithm. 35 | static var algorithm: JSONWebCompressionAlgorithm { get } 36 | 37 | /// Default buffer size. 38 | static var pageSize: Int { get } 39 | } 40 | 41 | /// JSON Web Compression Algorithms. 42 | @frozen 43 | public struct JSONWebCompressionAlgorithm: StringRepresentable { 44 | public let rawValue: String 45 | 46 | public init(rawValue: String) { 47 | self.rawValue = rawValue.trimmingCharacters(in: .whitespaces) 48 | } 49 | } 50 | 51 | extension JSONWebCompressionAlgorithm { 52 | #if canImport(Compression) 53 | private static let compressors: AtomicValue<[Self: any JSONWebCompressor.Type]> = [ 54 | .deflate: AppleCompressor.self, 55 | ] 56 | #elseif canImport(Czlib) || canImport(zlib) 57 | private static let compressors: AtomicValue<[Self: any JSONWebCompressor.Type]> = [ 58 | .deflate: ZlibCompressor.self, 59 | ] 60 | #else 61 | // This should never happen as Compression is available on Darwin platforms 62 | // and Zlib is used on non-Darwin platform. 63 | private static let compressors: AtomicValue<[Self: any JSONWebCompressor.Type]> = [:] 64 | #endif 65 | 66 | /// Returns provided compressor for this algorithm. 67 | public var compressor: (any JSONWebCompressor.Type)? { 68 | Self.compressors[self] 69 | } 70 | 71 | /// Currently registered algorithms. 72 | public static var registeredAlgorithms: [Self] { 73 | .init(compressors.keys) 74 | } 75 | 76 | /// Registers new compressor for given algorithm. 77 | /// 78 | /// - Parameters: 79 | /// - algorithm: Compression algorithm. 80 | /// - compressor: Compressor instance. 81 | public static func register(_ algorithm: Self, compressor: C.Type) where C: JSONWebCompressor { 82 | compressors[algorithm] = compressor 83 | } 84 | } 85 | 86 | extension JSONWebCompressionAlgorithm { 87 | /// Compression with the DEFLATE [RFC1951](https://www.rfc-editor.org/rfc/rfc1951) algorithm. 88 | public static let deflate: Self = "DEF" 89 | } 90 | 91 | /// Deflate (conforming to RFC 1951) 92 | public enum DeflateCompressionCodec: CompressionCodec { 93 | public static var algorithm: JSONWebCompressionAlgorithm { .deflate } 94 | public static var pageSize: Int { 65536 } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Algorithms/ContentEncryption.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentEncryption.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 10/13/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | /// JSON Web Key Encryption Algorithms 16 | @frozen 17 | public struct JSONWebContentEncryptionAlgorithm: JSONWebAlgorithm { 18 | public let rawValue: String 19 | 20 | public init(_ rawValue: S) where S: StringProtocol { 21 | self.rawValue = String(rawValue) 22 | } 23 | } 24 | 25 | extension JSONWebContentEncryptionAlgorithm { 26 | private static let keyRegistryClasses: AtomicValue<[Self: any JSONWebSymmetricSealingKey.Type]> = [ 27 | .integrated: JSONWebDirectKey.self, 28 | .aesEncryptionGCM128: JSONWebKeyAESGCM.self, 29 | .aesEncryptionGCM192: JSONWebKeyAESGCM.self, 30 | .aesEncryptionGCM256: JSONWebKeyAESGCM.self, 31 | .aesEncryptionCBC128SHA256: JSONWebKeyAESCBCHMAC.self, 32 | .aesEncryptionCBC192SHA384: JSONWebKeyAESCBCHMAC.self, 33 | .aesEncryptionCBC256SHA512: JSONWebKeyAESCBCHMAC.self, 34 | ] 35 | 36 | private static let keyLengths: AtomicValue<[Self: SymmetricKeySize]> = [ 37 | .aesEncryptionGCM128: .bits128, 38 | .aesEncryptionGCM192: .bits192, 39 | .aesEncryptionGCM256: .bits256, 40 | 41 | // AES-CBC-HMAC keys contains two keys, the first half is used 42 | // as HMAC key and the second half is as AES cipher key. 43 | .aesEncryptionCBC128SHA256: .bits128 * 2, 44 | .aesEncryptionCBC192SHA384: .bits192 * 2, 45 | .aesEncryptionCBC256SHA512: .bits256 * 2, 46 | ] 47 | 48 | /// Key type, either RSA, Elliptic curve, Symmetric, etc. 49 | public var keyType: JSONWebKeyType? { 50 | .symmetric 51 | } 52 | 53 | /// Returns sealing class appropriate for algorithm. 54 | public var keyClass: (any JSONWebSymmetricSealingKey.Type)? { 55 | Self.keyRegistryClasses[self] 56 | } 57 | 58 | // Length of key in bits. 59 | public var keyLength: SymmetricKeySize? { 60 | Self.keyLengths[self] 61 | } 62 | 63 | /// Currently registered algorithms. 64 | public static var registeredAlgorithms: [Self] { 65 | .init(keyRegistryClasses.keys) 66 | } 67 | 68 | /// Registers a new symmetric key for JWE content encryption. 69 | /// 70 | /// - Parameters: 71 | /// - algorithm: New algorithm name. 72 | /// - keyClass: Key class of symmetric key. 73 | /// - keyLength: The sizes that a symmetric cryptographic key can take. 74 | public static func register( 75 | _ algorithm: Self, 76 | keyClass: KT.Type, 77 | keyLength: SymmetricKeySize 78 | ) where KT: JSONWebSymmetricSealingKey { 79 | keyRegistryClasses[algorithm] = keyClass 80 | keyLengths[algorithm] = keyLength 81 | } 82 | } 83 | 84 | extension JSONWebContentEncryptionAlgorithm { 85 | /// Generates new random key with minimum key length. 86 | /// 87 | /// - Returns: New random key. 88 | public func generateRandomKey() throws -> any JSONWebSymmetricSealingKey { 89 | guard let keyClass = keyClass, let keyLength = keyLength else { 90 | throw JSONWebKeyError.unknownAlgorithm 91 | } 92 | return try keyClass.init(SymmetricKey(size: keyLength)) 93 | } 94 | } 95 | 96 | // Content Encryption 97 | extension JSONWebAlgorithm where Self == JSONWebContentEncryptionAlgorithm { 98 | /// **Content Encryption**: Encryption is provided by KEK directly, e.g. when HPKE JWE Integrated Encryption is used. 99 | public static var integrated: Self { "int" } 100 | 101 | /// **Content Encryption**: AES GCM using 128-bit key. 102 | public static var aesEncryptionGCM128: Self { "A128GCM" } 103 | 104 | /// **Content Encryption**: AES GCM using 192-bit key. 105 | public static var aesEncryptionGCM192: Self { "A192GCM" } 106 | 107 | /// **Content Encryption**: AES GCM using 256-bit key. 108 | public static var aesEncryptionGCM256: Self { "A256GCM" } 109 | 110 | static func aesEncryptionGCM(bitCount: Int) -> Self { 111 | .init(rawValue: "A\(bitCount)GCM") 112 | } 113 | 114 | /// **Content Encryption**: `AES_128_CBC_HMAC_SHA_256` authenticated encryption algorithm. 115 | public static var aesEncryptionCBC128SHA256: Self { "A128CBC-HS256" } 116 | 117 | /// **Content Encryption**: `AES_192_CBC_HMAC_SHA_384` authenticated encryption algorithm. 118 | public static var aesEncryptionCBC192SHA384: Self { "A192CBC-HS384" } 119 | 120 | /// **Content Encryption**: `AES_256_CBC_HMAC_SHA_512` authenticated encryption algorithm. 121 | public static var aesEncryptionCBC256SHA512: Self { "A256CBC-HS512" } 122 | 123 | static func aesEncryptionCBCSHA(bitCount: Int) -> Self { 124 | .init(rawValue: "A\(bitCount)CBC-HS\(bitCount * 2)") 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Certificate/JSONWebCertificate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONWebCertificate.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 2/6/24. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | #if canImport(X509) 15 | import X509 16 | #endif 17 | #if canImport(CommonCrypto) 18 | import CommonCrypto 19 | #endif 20 | 21 | #if canImport(X509) 22 | public typealias CertificateType = Certificate 23 | #elseif canImport(CommonCrypto) 24 | public typealias CertificateType = SecCertificate 25 | #else 26 | public typealias CertificateType = Data 27 | #endif 28 | 29 | #if canImport(X509) || canImport(CommonCrypto) 30 | /// JSON Web Key (JWK) container for X509 Certificate chain. 31 | /// 32 | /// - Important: Only `x5c` is supported. Loading from `x5u` is not supported now. 33 | @frozen 34 | public struct JSONWebCertificateChain: MutableJSONWebKey, JSONWebValidatingKey, Sendable { 35 | public var storage: JSONWebValueStorage 36 | 37 | public var leaf: CertificateType { 38 | get throws { 39 | try .init(from: self) 40 | } 41 | } 42 | 43 | public init(storage: JSONWebValueStorage) throws { 44 | self.storage = storage 45 | try validate() 46 | } 47 | 48 | public func validate() throws { 49 | // swiftformat:disable:next redundantSelf 50 | guard !self.certificateChain.isEmpty else { 51 | throw JSONWebKeyError.keyNotFound 52 | } 53 | } 54 | 55 | public func verifySignature(_ signature: S, for data: D, using algorithm: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol { 56 | try leaf.verifySignature(signature, for: data, using: algorithm) 57 | } 58 | 59 | public func thumbprint(format: JSONWebKeyFormat, using hashFunction: H.Type) throws -> H.Digest where H: HashFunction { 60 | try leaf.thumbprint(format: format, using: hashFunction) 61 | } 62 | } 63 | 64 | extension JSONWebCertificateChain: Expirable { 65 | public func verifyDate(_ currentDate: Date) throws { 66 | try leaf.verifyDate(currentDate) 67 | } 68 | } 69 | #endif 70 | 71 | #if canImport(X509) 72 | extension Verifier { 73 | public mutating func validate( 74 | chain: JSONWebCertificateChain, 75 | diagnosticCallback: ((VerificationDiagnostic) -> Void)? = nil 76 | ) async -> VerificationResult { 77 | do { 78 | return try await validate( 79 | leafCertificate: chain.leaf, 80 | intermediates: .init(chain.certificateChain.dropFirst()), 81 | diagnosticCallback: diagnosticCallback 82 | ) 83 | } catch { 84 | return .couldNotValidate([]) 85 | } 86 | } 87 | } 88 | #endif 89 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Compression/Apple.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Apple.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 10/23/23. 6 | // 7 | 8 | #if canImport(Compression) 9 | import Compression 10 | #if canImport(FoundationEssentials) 11 | import FoundationEssentials 12 | #else 13 | import Foundation 14 | #endif 15 | 16 | extension JSONWebCompressionAlgorithm { 17 | var appleAlgorithm: Algorithm { 18 | get throws { 19 | switch self { 20 | case .deflate: 21 | return .zlib 22 | default: 23 | throw JSONWebKeyError.unknownAlgorithm 24 | } 25 | } 26 | } 27 | } 28 | 29 | /// Compressor contain compress and decompress implementation using `Compression` framework. 30 | struct AppleCompressor: JSONWebCompressor, Sendable where Codec: CompressionCodec { 31 | static func compress(_ data: D) throws -> Data where D: DataProtocol { 32 | var compressedData = Data() 33 | let filter = try OutputFilter(.compress, using: Codec.algorithm.appleAlgorithm) { 34 | compressedData.append($0 ?? .init()) 35 | } 36 | 37 | // Compress the data 38 | try filter.write(data) 39 | try filter.finalize() 40 | return compressedData 41 | } 42 | 43 | static func decompress(_ data: D) throws -> Data where D: DataProtocol { 44 | var data = Data(data) 45 | var decompressedData = Data() 46 | let filter = try InputFilter(.decompress, using: Codec.algorithm.appleAlgorithm) { count in 47 | defer { data = data.dropFirst(count) } 48 | return data.prefix(count) 49 | } 50 | 51 | // Decompress the data 52 | while let chunk = try filter.readData(ofLength: Codec.pageSize), !chunk.isEmpty { 53 | decompressedData.append(chunk) 54 | } 55 | 56 | return decompressedData 57 | } 58 | } 59 | #endif 60 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Compression/Zlib.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Zlib.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 5/1/24. 6 | // 7 | 8 | #if canImport(Czlib) || canImport(zlib) 9 | #if canImport(FoundationEssentials) 10 | import FoundationEssentials 11 | #else 12 | import Foundation 13 | #endif 14 | #if canImport(Czlib) 15 | import Czlib 16 | #elseif canImport(zlib) 17 | import zlib 18 | #endif 19 | 20 | extension POSIXError { 21 | init(zlibStatus: Int32) { 22 | self = switch zlibStatus { 23 | case Z_ERRNO: 24 | POSIXError(.EIO, userInfo: [:]) 25 | case Z_STREAM_ERROR, 26 | Z_DATA_ERROR, 27 | Z_VERSION_ERROR: 28 | POSIXError(.EINVAL, userInfo: [:]) 29 | case Z_MEM_ERROR: 30 | POSIXError(.ENOMEM, userInfo: [:]) 31 | case Z_BUF_ERROR: 32 | POSIXError(.ENOBUFS, userInfo: [:]) 33 | default: 34 | POSIXError(.ECANCELED, userInfo: [:]) 35 | } 36 | } 37 | } 38 | 39 | @discardableResult 40 | private func zlibCall(_ handler: () -> Int32) throws -> Int32 { 41 | let status = handler() 42 | if status < Z_OK { 43 | throw POSIXError(zlibStatus: status) 44 | } 45 | return status 46 | } 47 | 48 | /// Compressor contain compress and decompress implementation using `Compression` framework. 49 | struct ZlibCompressor: JSONWebCompressor, Sendable where Codec: CompressionCodec { 50 | static func compress(_ data: D) throws -> Data where D: DataProtocol { 51 | var s = z_stream() 52 | try zlibCall { 53 | deflateInit2_(&s, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -15, 8, Z_DEFAULT_STRATEGY, ZLIB_VERSION, Int32(MemoryLayout.size)) 54 | } 55 | defer { deflateEnd(&s) } 56 | let outBuf = UnsafeMutableBufferPointer.allocate(capacity: Codec.pageSize) 57 | defer { outBuf.deallocate() } 58 | var compressed = Data() 59 | try data.withUnsafeBuffer { inBuf in 60 | s.next_in = .init(mutating: inBuf.baseAddress?.assumingMemoryBound(to: Bytef.self)) 61 | s.avail_in = .init(inBuf.count) 62 | while s.avail_in > 0 { 63 | s.next_out = outBuf.baseAddress 64 | s.avail_out = uInt(outBuf.count) 65 | try zlibCall { 66 | deflate(&s, Z_NO_FLUSH) 67 | } 68 | compressed.append(outBuf.baseAddress!, count: outBuf.count - Int(s.avail_out)) 69 | } 70 | } 71 | outBuf.initialize(repeating: 0) 72 | var status = Z_OK 73 | while status != Z_STREAM_END { 74 | s.next_out = outBuf.baseAddress 75 | s.avail_out = .init(outBuf.count) 76 | status = try zlibCall { 77 | deflate(&s, Z_FINISH) 78 | } 79 | compressed.append(outBuf.baseAddress!, count: outBuf.count - Int(s.avail_out)) 80 | } 81 | return compressed 82 | } 83 | 84 | static func decompress(_ data: D) throws -> Data where D: DataProtocol { 85 | var s = z_stream() 86 | try zlibCall { 87 | inflateInit2_(&s, -15, ZLIB_VERSION, Int32(MemoryLayout.size)) 88 | } 89 | defer { inflateEnd(&s) } 90 | 91 | var decompressed = Data() 92 | try data.withUnsafeBuffer { inBuf in 93 | s.next_in = .init(mutating: inBuf.baseAddress?.assumingMemoryBound(to: Bytef.self)) 94 | s.avail_in = uInt(inBuf.count) 95 | let outBuf = UnsafeMutableBufferPointer.allocate(capacity: Codec.pageSize) 96 | defer { outBuf.deallocate() } 97 | repeat { 98 | s.next_out = outBuf.baseAddress 99 | s.avail_out = uInt(outBuf.count) 100 | try zlibCall { 101 | inflate(&s, Z_NO_FLUSH) 102 | } 103 | decompressed.append(outBuf.baseAddress!, count: outBuf.count - Int(s.avail_out)) 104 | } while s.avail_out == 0 105 | } 106 | return decompressed 107 | } 108 | } 109 | #endif 110 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/EC/ECCAbstract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ECCAbstract.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/10/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | protocol CryptoECPublicKey: JSONWebKeyCurveType, JSONWebKeyRawRepresentable { 16 | static var curve: JSONWebKeyCurve { get } 17 | } 18 | 19 | extension CryptoECPublicKey { 20 | public var storage: JSONWebValueStorage { 21 | var result = AnyJSONWebKey() 22 | let rawRepresentation = rawRepresentation 23 | result.keyType = .ellipticCurve 24 | result.curve = Self.curve 25 | result.xCoordinate = rawRepresentation.prefix(rawRepresentation.count / 2) 26 | result.yCoordinate = rawRepresentation.suffix(rawRepresentation.count / 2) 27 | return result.storage 28 | } 29 | 30 | public init(storage: JSONWebValueStorage) throws { 31 | let keyData = AnyJSONWebKey(storage: storage) 32 | guard let x = keyData.xCoordinate, !x.isEmpty, let y = keyData.yCoordinate, y.count == x.count else { 33 | throw CryptoKitError.incorrectKeySize 34 | } 35 | try self.init(rawRepresentation: x + y) 36 | } 37 | } 38 | 39 | protocol CryptoECPrivateKey: JSONWebKeyCurveType, JSONWebPrivateKey, Hashable where PublicKey: CryptoECPublicKey { 40 | var rawRepresentation: Data { get } 41 | init(rawRepresentation: Data) throws 42 | } 43 | 44 | extension CryptoECPrivateKey { 45 | public var storage: JSONWebValueStorage { 46 | var result: some (MutableJSONWebKey & JSONWebKeyCurveType) = AnyJSONWebKey(publicKey) 47 | result.privateKey = rawRepresentation 48 | return result.storage 49 | } 50 | 51 | public init(storage: JSONWebValueStorage) throws { 52 | let keyData: some (MutableJSONWebKey & JSONWebKeyCurveType) = AnyJSONWebKey(storage: storage) 53 | guard let privateKey = keyData.privateKey, !privateKey.isEmpty else { 54 | throw CryptoKitError.incorrectKeySize 55 | } 56 | try self.init(rawRepresentation: privateKey) 57 | } 58 | 59 | public func thumbprint(format: JSONWebKeyFormat, using hashFunction: H.Type) throws -> H.Digest where H: HashFunction { 60 | try publicKey.thumbprint(format: format, using: hashFunction) 61 | } 62 | } 63 | 64 | protocol CryptoECKeyPortable: JSONWebKeyImportable, JSONWebKeyExportable { 65 | var x963Representation: Data { get } 66 | var derRepresentation: Data { get } 67 | 68 | init(x963Representation: Bytes) throws where Bytes: ContiguousBytes 69 | init(derRepresentation: Bytes) throws where Bytes: RandomAccessCollection, Bytes.Element == UInt8 70 | } 71 | 72 | protocol CryptoECKeyPortableCompactRepresentable: CryptoECKeyPortable { 73 | @available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) 74 | init(compressedRepresentation: Bytes) throws where Bytes: ContiguousBytes 75 | } 76 | 77 | extension CryptoECKeyPortable { 78 | public init(importing key: D, format: JSONWebKeyFormat) throws where D: DataProtocol { 79 | switch format { 80 | case .raw: 81 | try self.init(x963Representation: key.asContiguousBytes) 82 | case .spki where Self.self is (any CryptoECPublicKey).Type, 83 | .pkcs8 where Self.self is (any CryptoECPrivateKey).Type: 84 | try self.init(derRepresentation: key) 85 | case .jwk: 86 | self = try JSONDecoder().decode(Self.self, from: Data(key)) 87 | default: 88 | throw JSONWebKeyError.invalidKeyFormat 89 | } 90 | } 91 | 92 | public func exportKey(format: JSONWebKeyFormat) throws -> Data { 93 | switch format { 94 | case .raw: 95 | return x963Representation 96 | case .spki where self is any CryptoECPublicKey, 97 | .pkcs8 where self is any CryptoECPrivateKey: 98 | return derRepresentation 99 | case .jwk: 100 | return try jwkRepresentation 101 | default: 102 | throw JSONWebKeyError.invalidKeyFormat 103 | } 104 | } 105 | } 106 | 107 | extension CryptoECKeyPortableCompactRepresentable { 108 | private init(importingRaw key: D) throws where D: DataProtocol { 109 | switch key.first { 110 | case 0x04: 111 | try self.init(x963Representation: key.asContiguousBytes) 112 | case 0x02, 0x03: 113 | if #available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) { 114 | try self.init(compressedRepresentation: key.asContiguousBytes) 115 | } else { 116 | throw CryptoKitError.incorrectParameterSize 117 | } 118 | default: 119 | throw CryptoKitError.incorrectParameterSize 120 | } 121 | } 122 | 123 | public init(importing key: D, format: JSONWebKeyFormat) throws where D: DataProtocol { 124 | switch format { 125 | case .raw: 126 | try self.init(importingRaw: key) 127 | case .spki where Self.self is (any CryptoECPublicKey).Type, 128 | .pkcs8 where Self.self is (any CryptoECPrivateKey).Type: 129 | try self.init(derRepresentation: key) 130 | case .jwk: 131 | self = try JSONDecoder().decode(Self.self, from: Data(key)) 132 | default: 133 | throw JSONWebKeyError.invalidKeyFormat 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/EC/P256.swift: -------------------------------------------------------------------------------- 1 | // 2 | // P256.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/9/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | extension Crypto.P256.Signing.PublicKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 16 | 17 | extension P256.Signing.PublicKey: CryptoECPublicKey, JSONWebKeyAlgorithmIdentified { 18 | public static var algorithm: any JSONWebAlgorithm { .ecdsaSignatureP256SHA256 } 19 | public static var algorithmIdentifier: RFC5480AlgorithmIdentifier { .ecdsaP256 } 20 | static var curve: JSONWebKeyCurve { .p256 } 21 | } 22 | 23 | extension Crypto.P256.KeyAgreement.PublicKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 24 | 25 | extension P256.KeyAgreement.PublicKey: CryptoECPublicKey { 26 | static var curve: JSONWebKeyCurve { .p256 } 27 | } 28 | 29 | extension P256.Signing.PublicKey: JSONWebValidatingKey { 30 | public func verifySignature(_ signature: S, for data: D, using _: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol { 31 | let ecdsaSignature: P256.Signing.ECDSASignature 32 | // swiftformat:disable:next redundantSelf 33 | if signature.count == (self.curve?.coordinateSize ?? 0) * 2 { 34 | ecdsaSignature = try .init(rawRepresentation: signature) 35 | } else { 36 | ecdsaSignature = try .init(derRepresentation: signature) 37 | } 38 | if !isValidSignature(ecdsaSignature, for: SHA256.hash(data: data)) { 39 | throw CryptoKitError.authenticationFailure 40 | } 41 | } 42 | } 43 | 44 | extension P256.Signing.PublicKey: CryptoECKeyPortableCompactRepresentable {} 45 | 46 | extension P256.KeyAgreement.PublicKey: CryptoECKeyPortableCompactRepresentable {} 47 | 48 | extension Crypto.P256.Signing.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 49 | 50 | extension P256.Signing.PrivateKey: JSONWebSigningKey, JSONWebKeyAlgorithmIdentified, CryptoECPrivateKey { 51 | public typealias PublicKey = P256.Signing.PublicKey 52 | 53 | public init(algorithm _: some JSONWebAlgorithm) throws { 54 | self.init(compactRepresentable: false) 55 | } 56 | 57 | public func signature(_ data: D, using algorithm: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 58 | guard let hashFunction = algorithm.hashFunction else { 59 | throw JSONWebKeyError.unknownAlgorithm 60 | } 61 | return try signature(for: hashFunction.hash(data: data)).rawRepresentation 62 | } 63 | } 64 | 65 | extension Crypto.P256.KeyAgreement.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable { 66 | public func hash(into hasher: inout Hasher) { 67 | hasher.combine(publicKey) 68 | } 69 | } 70 | 71 | extension P256.KeyAgreement.PrivateKey: CryptoECPrivateKey { 72 | public typealias PublicKey = P256.KeyAgreement.PublicKey 73 | public init(algorithm _: some JSONWebAlgorithm) throws { 74 | self.init(compactRepresentable: false) 75 | } 76 | } 77 | 78 | extension P256.Signing.PrivateKey: CryptoECKeyPortable {} 79 | 80 | extension P256.KeyAgreement.PrivateKey: CryptoECKeyPortable {} 81 | 82 | #if canImport(Darwin) 83 | extension Crypto.SecureEnclave.P256.Signing.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 84 | 85 | extension SecureEnclave.P256.Signing.PrivateKey: JSONWebSigningKey, JSONWebKeyAlgorithmIdentified, CryptoECPrivateKey { 86 | public typealias PublicKey = P256.Signing.PublicKey 87 | 88 | public var storage: JSONWebValueStorage { 89 | // Keys stored in SecureEnclave are not exportable. 90 | // 91 | // In order to get key type and other necessary information in signing 92 | // process, public key is returned which contains these values. 93 | publicKey.storage 94 | } 95 | 96 | var rawRepresentation: Data { 97 | assertionFailure("Private Keys in Secure Enclave are not encodable.") 98 | return publicKey.rawRepresentation 99 | } 100 | 101 | public init(algorithm _: some JSONWebAlgorithm) throws { 102 | try self.init(compactRepresentable: true) 103 | } 104 | 105 | init(rawRepresentation _: Data) throws { 106 | throw JSONWebKeyError.operationNotAllowed 107 | } 108 | 109 | public func signature(_ data: D, using _: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 110 | try signature(for: SHA256.hash(data: data)).rawRepresentation 111 | } 112 | } 113 | 114 | extension Crypto.SecureEnclave.P256.KeyAgreement.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable { 115 | public func hash(into hasher: inout Hasher) { 116 | hasher.combine(publicKey) 117 | } 118 | } 119 | 120 | extension SecureEnclave.P256.KeyAgreement.PrivateKey: CryptoECPrivateKey { 121 | public typealias PublicKey = P256.KeyAgreement.PublicKey 122 | 123 | var rawRepresentation: Data { 124 | assertionFailure("Private Keys in Secure Enclave are not encodable.") 125 | return publicKey.rawRepresentation 126 | } 127 | 128 | init(rawRepresentation _: Data) throws { 129 | throw JSONWebKeyError.operationNotAllowed 130 | } 131 | } 132 | #endif 133 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/EC/P384.swift: -------------------------------------------------------------------------------- 1 | // 2 | // P384.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/9/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | extension Crypto.P384.Signing.PublicKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 16 | 17 | extension P384.Signing.PublicKey: CryptoECPublicKey, JSONWebKeyAlgorithmIdentified { 18 | public static var algorithm: any JSONWebAlgorithm { .ecdsaSignatureP384SHA384 } 19 | public static var algorithmIdentifier: RFC5480AlgorithmIdentifier { .ecdsaP384 } 20 | static var curve: JSONWebKeyCurve { .p384 } 21 | } 22 | 23 | extension Crypto.P384.KeyAgreement.PublicKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 24 | 25 | extension P384.KeyAgreement.PublicKey: CryptoECPublicKey { 26 | static var curve: JSONWebKeyCurve { .p384 } 27 | } 28 | 29 | extension P384.Signing.PublicKey: JSONWebValidatingKey { 30 | public func verifySignature(_ signature: S, for data: D, using _: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol { 31 | let ecdsaSignature: P384.Signing.ECDSASignature 32 | // swiftformat:disable:next redundantSelf 33 | if signature.count == (self.curve?.coordinateSize ?? 0) * 2 { 34 | ecdsaSignature = try .init(rawRepresentation: signature) 35 | } else { 36 | ecdsaSignature = try .init(derRepresentation: signature) 37 | } 38 | if !isValidSignature(ecdsaSignature, for: SHA384.hash(data: data)) { 39 | throw CryptoKitError.authenticationFailure 40 | } 41 | } 42 | } 43 | 44 | extension P384.Signing.PublicKey: CryptoECKeyPortableCompactRepresentable {} 45 | 46 | extension P384.KeyAgreement.PublicKey: CryptoECKeyPortableCompactRepresentable {} 47 | 48 | extension Crypto.P384.Signing.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 49 | 50 | extension P384.Signing.PrivateKey: JSONWebSigningKey, JSONWebKeyAlgorithmIdentified, CryptoECPrivateKey { 51 | public typealias PublicKey = P384.Signing.PublicKey 52 | 53 | public init(algorithm _: some JSONWebAlgorithm) throws { 54 | self.init(compactRepresentable: false) 55 | } 56 | 57 | public func signature(_ data: D, using algorithm: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 58 | guard let hashFunction = algorithm.hashFunction else { 59 | throw JSONWebKeyError.unknownAlgorithm 60 | } 61 | return try signature(for: hashFunction.hash(data: data)).rawRepresentation 62 | } 63 | } 64 | 65 | extension Crypto.P384.KeyAgreement.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable { 66 | public func hash(into hasher: inout Hasher) { 67 | hasher.combine(publicKey) 68 | } 69 | } 70 | 71 | extension P384.KeyAgreement.PrivateKey: CryptoECPrivateKey { 72 | public typealias PublicKey = P384.KeyAgreement.PublicKey 73 | 74 | public init(algorithm _: some JSONWebAlgorithm) throws { 75 | self.init(compactRepresentable: false) 76 | } 77 | } 78 | 79 | extension P384.Signing.PrivateKey: CryptoECKeyPortable {} 80 | 81 | extension P384.KeyAgreement.PrivateKey: CryptoECKeyPortable {} 82 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/EC/P521.swift: -------------------------------------------------------------------------------- 1 | // 2 | // P521.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/9/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | extension Crypto.P521.Signing.PublicKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 16 | 17 | extension P521.Signing.PublicKey: CryptoECPublicKey, JSONWebKeyAlgorithmIdentified { 18 | static var curve: JSONWebKeyCurve { .p521 } 19 | public static var algorithm: any JSONWebAlgorithm { .ecdsaSignatureP521SHA512 } 20 | public static var algorithmIdentifier: RFC5480AlgorithmIdentifier { .ecdsaP521 } 21 | } 22 | 23 | extension Crypto.P521.KeyAgreement.PublicKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 24 | 25 | extension P521.KeyAgreement.PublicKey: CryptoECPublicKey { 26 | static var curve: JSONWebKeyCurve { .p521 } 27 | } 28 | 29 | extension P521.Signing.PublicKey: JSONWebValidatingKey { 30 | public func verifySignature(_ signature: S, for data: D, using _: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol { 31 | let ecdsaSignature: P521.Signing.ECDSASignature 32 | // swiftformat:disable:next redundantSelf 33 | if signature.count == (self.curve?.coordinateSize ?? 0) * 2 { 34 | ecdsaSignature = try .init(rawRepresentation: signature) 35 | } else { 36 | ecdsaSignature = try .init(derRepresentation: signature) 37 | } 38 | if !isValidSignature(ecdsaSignature, for: SHA512.hash(data: data)) { 39 | throw CryptoKitError.authenticationFailure 40 | } 41 | } 42 | } 43 | 44 | extension P521.Signing.PublicKey: CryptoECKeyPortableCompactRepresentable {} 45 | 46 | extension P521.KeyAgreement.PublicKey: CryptoECKeyPortableCompactRepresentable {} 47 | 48 | extension Crypto.P521.Signing.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 49 | 50 | extension P521.Signing.PrivateKey: JSONWebSigningKey, JSONWebKeyAlgorithmIdentified, CryptoECPrivateKey { 51 | public typealias PublicKey = P521.Signing.PublicKey 52 | 53 | public init(algorithm _: some JSONWebAlgorithm) throws { 54 | self.init(compactRepresentable: false) 55 | } 56 | 57 | public func signature(_ data: D, using algorithm: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 58 | guard let hashFunction = algorithm.hashFunction else { 59 | throw JSONWebKeyError.unknownAlgorithm 60 | } 61 | return try signature(for: hashFunction.hash(data: data)).rawRepresentation 62 | } 63 | } 64 | 65 | extension Crypto.P521.KeyAgreement.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable { 66 | public func hash(into hasher: inout Hasher) { 67 | hasher.combine(publicKey) 68 | } 69 | } 70 | 71 | extension P521.KeyAgreement.PrivateKey: CryptoECPrivateKey { 72 | public typealias PublicKey = P521.KeyAgreement.PublicKey 73 | 74 | public init(algorithm _: some JSONWebAlgorithm) throws { 75 | self.init(compactRepresentable: false) 76 | } 77 | } 78 | 79 | extension P521.Signing.PrivateKey: CryptoECKeyPortable {} 80 | 81 | extension P521.KeyAgreement.PrivateKey: CryptoECKeyPortable {} 82 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Hashing/SHA.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SHA.swift 3 | // JWSETKit 4 | // 5 | // Created by Amir Abbas Mousavian on 9/10/25. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | /// Hash name according to [RFC6920](https://www.rfc-editor.org/rfc/rfc6920 ). 16 | public protocol NamedHashFunction: HashFunction { 17 | /// [IANA registration name](https://www.iana.org/assignments/named-information/named-information.xhtml) of the digest algorithm. 18 | static var identifier: JSONWebHashAlgorithm { get } 19 | } 20 | 21 | extension SHA256: NamedHashFunction { 22 | public static let identifier: JSONWebHashAlgorithm = "sha-256" 23 | } 24 | 25 | extension SHA384: NamedHashFunction { 26 | public static let identifier: JSONWebHashAlgorithm = "sha-384" 27 | } 28 | 29 | extension SHA512: NamedHashFunction { 30 | public static let identifier: JSONWebHashAlgorithm = "sha-512" 31 | } 32 | 33 | #if canImport(CryptoKit) && compiler(>=6.2) 34 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 35 | extension SHA3_256: NamedHashFunction { 36 | public static let identifier: JSONWebHashAlgorithm = "sha3-256" 37 | } 38 | 39 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 40 | extension SHA3_384: NamedHashFunction { 41 | public static let identifier: JSONWebHashAlgorithm = "sha3-384" 42 | } 43 | 44 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 45 | extension SHA3_512: NamedHashFunction { 46 | public static let identifier: JSONWebHashAlgorithm = "sha3-512" 47 | } 48 | #endif 49 | 50 | /// JSON Web Compression Algorithms. 51 | @frozen 52 | public struct JSONWebHashAlgorithm: StringRepresentable { 53 | public let rawValue: String 54 | 55 | public init(rawValue: String) { 56 | self.rawValue = rawValue.trimmingCharacters(in: .whitespaces) 57 | } 58 | } 59 | 60 | extension JSONWebHashAlgorithm { 61 | private static let hashFunctions: AtomicValue<[Self: any HashFunction.Type]> = { 62 | if #available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) { 63 | #if canImport(CryptoKit) && compiler(>=6.2) 64 | [ 65 | SHA256.identifier: SHA256.self, 66 | SHA384.identifier: SHA384.self, 67 | SHA512.identifier: SHA512.self, 68 | SHA3_256.identifier: SHA3_256.self, 69 | SHA3_384.identifier: SHA3_384.self, 70 | SHA3_512.identifier: SHA3_512.self, 71 | ] 72 | #else 73 | [ 74 | SHA256.identifier: SHA256.self, 75 | SHA384.identifier: SHA384.self, 76 | SHA512.identifier: SHA512.self, 77 | ] 78 | #endif 79 | } else { 80 | [ 81 | SHA256.identifier: SHA256.self, 82 | SHA384.identifier: SHA384.self, 83 | SHA512.identifier: SHA512.self, 84 | ] 85 | } 86 | }() 87 | 88 | /// Returns provided hash function for this algorithm. 89 | public var hashFunction: (any HashFunction.Type)? { 90 | Self.hashFunctions[self] 91 | } 92 | 93 | /// Currently registered algorithms. 94 | public static var registeredAlgorithms: [Self] { 95 | .init(hashFunctions.keys) 96 | } 97 | 98 | /// Registers new hash function for given algorithm. 99 | /// 100 | /// - Parameters: 101 | /// - algorithm: hash function algorithm. 102 | /// - hashFunction: hash function type. 103 | public static func register(_ algorithm: Self, hashFunction: C.Type) where C: HashFunction { 104 | hashFunctions[algorithm] = hashFunction 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/PQC/MLDSA65.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MLDSA65.swift 3 | // JWSETKit 4 | // 5 | // Created by Amir Abbas Mousavian on 7/16/25. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | // TODO: Remove condition after release of swift-crypto 4.0 15 | #if canImport(CryptoKit) && compiler(>=6.2) 16 | import Crypto 17 | 18 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 19 | extension Crypto.MLDSA65.PublicKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 20 | 21 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 22 | extension MLDSA65.PublicKey: JSONWebValidatingKey, JSONWebKeyRawRepresentable, JSONWebKeyImportable, JSONWebKeyExportable, CryptoModuleLatticePublicKey { 23 | public static let algorithm: any JSONWebAlgorithm = .mldsa65Signature 24 | 25 | public static let algorithmIdentifier: RFC5480AlgorithmIdentifier = .mldsa65 26 | } 27 | 28 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 29 | extension Crypto.MLDSA65.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 30 | 31 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 32 | extension MLDSA65.PrivateKey: JSONWebSigningKey, JSONWebKeyImportable, JSONWebKeyExportable, CryptoModuleLatticePrivateKey {} 33 | 34 | #if canImport(Darwin) && compiler(>=6.2) 35 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 36 | extension Crypto.SecureEnclave.MLDSA65.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 37 | 38 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 39 | extension SecureEnclave.MLDSA65.PrivateKey: JSONWebSigningKey { 40 | public var storage: JSONWebValueStorage { 41 | // Keys stored in SecureEnclave are not exportable. 42 | // 43 | // In order to get key type and other necessary information in signing 44 | // process, public key is returned which contains these values. 45 | publicKey.storage 46 | } 47 | 48 | public init(algorithm _: some JSONWebAlgorithm) throws { 49 | try self.init() 50 | } 51 | 52 | public init(storage _: JSONWebValueStorage) throws { 53 | throw JSONWebKeyError.operationNotAllowed 54 | } 55 | 56 | public func signature(_ data: D, using _: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 57 | try signature(for: data) 58 | } 59 | } 60 | #endif 61 | #endif 62 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/PQC/MLDSA87.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MLDSA87.swift 3 | // JWSETKit 4 | // 5 | // Created by Amir Abbas Mousavian on 7/16/25. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | // TODO: Remove condition after release of swift-crypto 4.0 15 | #if canImport(CryptoKit) && compiler(>=6.2) 16 | import Crypto 17 | 18 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 19 | extension Crypto.MLDSA87.PublicKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 20 | 21 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 22 | extension MLDSA87.PublicKey: JSONWebValidatingKey, JSONWebKeyRawRepresentable, JSONWebKeyImportable, JSONWebKeyExportable, CryptoModuleLatticePublicKey { 23 | public static let algorithm: any JSONWebAlgorithm = .mldsa87Signature 24 | 25 | public static let algorithmIdentifier: RFC5480AlgorithmIdentifier = .mldsa87 26 | } 27 | 28 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 29 | extension Crypto.MLDSA87.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 30 | 31 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 32 | extension MLDSA87.PrivateKey: JSONWebSigningKey, JSONWebKeyImportable, JSONWebKeyExportable, CryptoModuleLatticePrivateKey {} 33 | 34 | #if canImport(Darwin) && compiler(>=6.2) 35 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 36 | extension Crypto.SecureEnclave.MLDSA87.PrivateKey: Swift.Hashable, Swift.Equatable, Swift.Decodable, Swift.Encodable {} 37 | 38 | @available(iOS 26.0, macOS 26.0, watchOS 26.0, tvOS 26.0, *) 39 | extension SecureEnclave.MLDSA87.PrivateKey: JSONWebSigningKey { 40 | public var storage: JSONWebValueStorage { 41 | // Keys stored in SecureEnclave are not exportable. 42 | // 43 | // In order to get key type and other necessary information in signing 44 | // process, public key is returned which contains these values. 45 | publicKey.storage 46 | } 47 | 48 | public init(algorithm _: some JSONWebAlgorithm) throws { 49 | try self.init() 50 | } 51 | 52 | public init(storage _: JSONWebValueStorage) throws { 53 | throw JSONWebKeyError.operationNotAllowed 54 | } 55 | 56 | public func signature(_ data: D, using _: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 57 | try signature(for: data) 58 | } 59 | } 60 | #endif 61 | #endif 62 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/PQC/ModuleLatticeAbstract.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModuleLatticeAbstract.swift 3 | // JWSETKit 4 | // 5 | // Created by Amir Abbas Mousavian on 6/17/25. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | protocol CryptoModuleLatticePublicKey: JSONWebKey, JSONWebKeyRawRepresentable, JSONWebKeyAlgorithmIdentified, JSONWebKeyImportable, JSONWebKeyExportable { 16 | func isValidSignature(_ signature: S, for data: D) -> Bool where S: DataProtocol, D: DataProtocol 17 | } 18 | 19 | extension CryptoModuleLatticePublicKey { 20 | public var storage: JSONWebValueStorage { 21 | var result = AnyJSONWebKey() 22 | result.keyType = .algorithmKeyPair 23 | result.algorithm = Self.algorithm 24 | result.publicKeyData = rawRepresentation 25 | return result.storage 26 | } 27 | 28 | public init(storage: JSONWebValueStorage) throws { 29 | let key = AnyJSONWebKey(storage: storage) 30 | guard let publicKey = key.publicKeyData else { 31 | throw CryptoKitError.incorrectParameterSize 32 | } 33 | try self.init(rawRepresentation: publicKey) 34 | } 35 | 36 | public func verifySignature(_ signature: S, for data: D, using _: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol { 37 | if !isValidSignature(signature, for: data) { 38 | throw CryptoKitError.authenticationFailure 39 | } 40 | } 41 | 42 | public func exportKey(format: JSONWebKeyFormat) throws -> Data { 43 | return switch format { 44 | case .raw: 45 | rawRepresentation 46 | case .spki where !(self is any JSONWebPrivateKey): 47 | try SubjectPublicKeyInfo( 48 | algorithmIdentifier: Self.algorithmIdentifier, 49 | key: [UInt8](rawRepresentation) 50 | ).derRepresentation 51 | case .pkcs8: 52 | throw JSONWebKeyError.operationNotAllowed 53 | case .jwk: 54 | try jwkRepresentation 55 | default: 56 | throw JSONWebKeyError.invalidKeyFormat 57 | } 58 | } 59 | } 60 | 61 | protocol CryptoModuleLatticePrivateKey: JSONWebPrivateKey, JSONWebKeyImportable, JSONWebKeyExportable where PublicKey: CryptoModuleLatticePublicKey { 62 | var seedRepresentation: Data { get } 63 | init() throws 64 | init(seedRepresentation: D, publicKey: PublicKey?) throws where D: DataProtocol 65 | func signature(for data: D) throws -> Data where D: DataProtocol 66 | } 67 | 68 | extension CryptoModuleLatticePrivateKey { 69 | public static var algorithmIdentifier: RFC5480AlgorithmIdentifier { 70 | PublicKey.algorithmIdentifier 71 | } 72 | 73 | public var storage: JSONWebValueStorage { 74 | var result = AnyJSONWebKey() 75 | result.keyType = .algorithmKeyPair 76 | result.algorithm = Self.PublicKey.algorithm 77 | result.publicKeyData = publicKey.rawRepresentation 78 | result.seed = seedRepresentation 79 | return result.storage 80 | } 81 | 82 | public init(storage: JSONWebValueStorage) throws { 83 | let key = AnyJSONWebKey(storage: storage) 84 | guard let seed = key.seed else { 85 | throw CryptoKitError.incorrectParameterSize 86 | } 87 | let publicKey = key.publicKeyData 88 | try self.init(seedRepresentation: seed, publicKey: publicKey.map(PublicKey.init(rawRepresentation:))) 89 | } 90 | 91 | public init(algorithm _: some JSONWebAlgorithm) throws { 92 | try self.init() 93 | } 94 | 95 | public init(importing key: D, format: JSONWebKeyFormat) throws where D: DataProtocol { 96 | switch format { 97 | case .raw: 98 | try self.init(seedRepresentation: key, publicKey: nil) 99 | case .spki: 100 | throw JSONWebKeyError.invalidKeyFormat 101 | case .pkcs8: 102 | let pkcs8 = try PKCS8PrivateKey(derEncoded: key) 103 | guard let privateKey = pkcs8.privateKey as? ModuleLatticePrivateKey else { 104 | throw JSONWebKeyError.invalidKeyFormat 105 | } 106 | self = try .init(seedRepresentation: [UInt8](privateKey.seed.bytes), publicKey: nil) 107 | case .jwk: 108 | self = try JSONDecoder().decode(Self.self, from: Data(key)) 109 | } 110 | } 111 | 112 | public func signature(_ data: D, using _: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 113 | try signature(for: data) 114 | } 115 | 116 | public func exportKey(format: JSONWebKeyFormat) throws -> Data { 117 | return switch format { 118 | case .raw: 119 | seedRepresentation 120 | case .spki: 121 | throw JSONWebKeyError.operationNotAllowed 122 | case .pkcs8: 123 | try PKCS8PrivateKey( 124 | algorithm: Self.algorithmIdentifier, 125 | privateKey: ModuleLatticePrivateKey(seed: .init(seedRepresentation)) 126 | ).derRepresentation 127 | case .jwk: 128 | try jwkRepresentation 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Symmetric/ConcatKDF.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConcatKDF.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 2/14/24. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | extension SharedSecret { 16 | /// Derives a symmetric encryption key from the secret using NIST SP800-56Ar2 section 5.8.1 17 | /// derivation. 18 | /// 19 | /// - Parameters: 20 | /// - hashFunction: The hash function to use for key derivation. 21 | /// - otherInfo: The other information to use for key derivation. 22 | /// - outputByteCount: The length in bytes of resulting symmetric key. 23 | /// 24 | /// - Returns: The derived symmetric key. 25 | public func concatDerivedSymmetricKey( 26 | using hashFunction: H.Type, 27 | otherInfo: OI, 28 | outputByteCount keySize: Int 29 | ) -> SymmetricKey where H: HashFunction, OI: DataProtocol { 30 | let hashSize = hashFunction.Digest.byteCount * 8 31 | let iterations = (keySize / hashSize) + (!keySize.isMultiple(of: hashSize) ? 1 : 0) 32 | 33 | let derivedKey = (1 ... iterations).reduce(Data()) { partialResult, counter in 34 | var hash = H() 35 | hash.update(UInt32(counter).bigEndian) 36 | withUnsafeBytes { hash.update(bufferPointer: $0) } 37 | hash.update(data: otherInfo) 38 | return partialResult + hash.finalize().data 39 | } 40 | return .init(data: derivedKey.trim(bitCount: keySize)) 41 | } 42 | 43 | func concatDerivedSymmetricKey( 44 | using hashFunction: H.Type, 45 | algorithm: JSONWebKeyEncryptionAlgorithm, 46 | contentEncryptionAlgorithm: JSONWebContentEncryptionAlgorithm?, 47 | apu: APU, 48 | apv: APV 49 | ) throws -> SymmetricKey where H: HashFunction, APU: DataProtocol, APV: DataProtocol { 50 | let algorithmID: String 51 | let keySize: Int 52 | if let length = algorithm.keyLength { 53 | algorithmID = algorithm.rawValue 54 | keySize = length 55 | } else { 56 | guard let cek = contentEncryptionAlgorithm, let contentKeySize = cek.keyLength?.bitCount else { 57 | throw CryptoKitError.incorrectKeySize 58 | } 59 | algorithmID = cek.rawValue 60 | keySize = contentKeySize 61 | } 62 | let algorithm = Data(algorithmID.utf8) 63 | return concatDerivedSymmetricKey( 64 | using: hashFunction, 65 | otherInfo: Data([ 66 | algorithm.lengthBytes, algorithm, 67 | apu.lengthBytes, Data(apu), 68 | apv.lengthBytes, Data(apv), 69 | Data(value: UInt32(keySize).bigEndian), // <- suppPubInfo 70 | ].joined()), 71 | outputByteCount: keySize 72 | ) 73 | } 74 | } 75 | 76 | extension MutableDataProtocol { 77 | fileprivate func trim(bitCount: Int) -> Self { 78 | var result = self 79 | if bitCount.isMultiple(of: 8) { 80 | result.removeLast(count - bitCount / 8) 81 | } else { 82 | result.removeLast(count - bitCount / 8 - 1) 83 | result[result.index(before: result.endIndex)] &= ~(0xFF >> (UInt(bitCount) % 8)) 84 | } 85 | 86 | return result 87 | } 88 | } 89 | 90 | extension RandomAccessCollection where Self.Element == UInt8 { 91 | fileprivate var lengthBytes: Data { 92 | Data(value: UInt32(count).bigEndian) 93 | } 94 | } 95 | 96 | extension HashFunction { 97 | @inlinable 98 | mutating func update(_ value: T) { 99 | withUnsafeBytes(of: value) { 100 | self.update(bufferPointer: $0) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Symmetric/Direct.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Direct.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 10/14/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | struct JSONWebDirectKey: JSONWebDecryptingKey, JSONWebSigningKey, JSONWebSymmetricSealingKey { 16 | let storage: JSONWebValueStorage 17 | 18 | var publicKey: Self { 19 | self 20 | } 21 | 22 | init(algorithm _: some JSONWebAlgorithm) throws { 23 | self.init(storage: .init()) 24 | } 25 | 26 | init(_: SymmetricKey) throws { 27 | self.init(storage: .init()) 28 | } 29 | 30 | init() throws { 31 | self.init(storage: .init()) 32 | } 33 | 34 | init(storage _: JSONWebValueStorage) { 35 | self.storage = .init() 36 | } 37 | 38 | func encrypt(_ data: D, using _: JWA) throws -> Data where D: DataProtocol, JWA: JSONWebAlgorithm { 39 | Data(data) 40 | } 41 | 42 | func decrypt(_ data: D, using _: JWA) throws -> Data where D: DataProtocol, JWA: JSONWebAlgorithm { 43 | Data(data) 44 | } 45 | 46 | func verifySignature(_ signature: S, for _: D, using _: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol { 47 | guard signature.isEmpty else { 48 | throw CryptoKitError.authenticationFailure 49 | } 50 | } 51 | 52 | func signature(_: D, using _: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 53 | .init() 54 | } 55 | 56 | func seal(_ data: D, iv _: IV?, authenticating _: AAD?, using _: JWA) throws -> SealedData where D: DataProtocol, IV: DataProtocol, AAD: DataProtocol, JWA: JSONWebAlgorithm { 57 | try .init(combined: data, nonceLength: 0, tagLength: 0) 58 | } 59 | 60 | func open(_ data: SealedData, authenticating _: AAD?, using _: JWA) throws -> Data where AAD: DataProtocol, JWA: JSONWebAlgorithm { 61 | data.ciphertext 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Symmetric/HMAC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HMAC.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/7/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | /// JSON Web Key (JWK) container for creating/verifying HMAC signatures. 16 | @frozen 17 | public struct JSONWebKeyHMAC: MutableJSONWebKey, JSONWebSymmetricSigningKey, Sendable { 18 | public var storage: JSONWebValueStorage 19 | 20 | /// Returns a new concrete key using json data. 21 | /// 22 | /// - Parameter storage: Storage of key-values. 23 | public init(storage: JSONWebValueStorage) throws { 24 | self.storage = storage 25 | try validate() 26 | } 27 | 28 | /// Returns a new HMAC key with given symmetric key. 29 | /// 30 | /// - Parameter key: Symmetric key for operation. 31 | public init(_ key: SymmetricKey) throws { 32 | self.storage = .init() 33 | self.keyType = .symmetric 34 | self.algorithm = .hmac(bitCount: H.Digest.byteCount * 8) 35 | self.keyValue = key.keyValue 36 | } 37 | 38 | public init(algorithm _: some JSONWebAlgorithm) throws { 39 | try self.init(SymmetricKey(size: .init(bitCount: H.Digest.byteCount * 8))) 40 | } 41 | 42 | public func signature(_ data: D, using _: JSONWebSignatureAlgorithm) throws -> Data { 43 | var hmac = try HMAC(key: .init(self)) 44 | hmac.update(data: data) 45 | let mac = hmac.finalize() 46 | return Data(mac) 47 | } 48 | 49 | public func verifySignature(_ signature: S, for data: D, using _: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol { 50 | let isValid = try HMAC.isValidAuthenticationCode(Data(signature), authenticating: data, using: .init(self)) 51 | guard isValid else { 52 | throw CryptoKitError.authenticationFailure 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Symmetric/PBKDF2.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PBKDF2.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 10/11/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | #if canImport(_CryptoExtras) 15 | import _CryptoExtras 16 | #endif 17 | 18 | extension SymmetricKey { 19 | static let defaultPBES2IterationCount: [Int: Int] = [ 20 | 128: 600_000, 21 | 192: 450_000, 22 | 256: 310_000, 23 | ] 24 | 25 | /// Generates a symmetric key using `PBKDF2` algorithm. 26 | /// 27 | /// - Parameters: 28 | /// - password: The master password from which a derived key is generated. 29 | /// - salt: A sequence of bits, known as a cryptographic salt. 30 | /// - hashFunction: Pseudorandom function algorithm. 31 | /// - iterations: Iteration count, a positive integer. 32 | /// 33 | /// - Returns: A symmetric key derived from parameters. 34 | public static func passwordBased2DerivedSymmetricKey( 35 | password: PD, salt: SD, iterations: Int, length: Int? = nil, hashFunction: H.Type 36 | ) throws -> SymmetricKey where PD: DataProtocol, SD: DataProtocol, H: HashFunction { 37 | let length = length ?? hashFunction.Digest.byteCount 38 | #if canImport(CommonCrypto) 39 | return try ccPbkdf2(pbkdf2Password: password, salt: salt, iterations: iterations, length: length, hashFunction: hashFunction) 40 | #elseif canImport(_CryptoExtras) 41 | return try KDF.Insecure.PBKDF2.deriveKey(from: password, salt: salt, using: .init(hashFunction), outputByteCount: length, unsafeUncheckedRounds: iterations) 42 | #else 43 | #error("Unimplemented") 44 | #endif 45 | } 46 | } 47 | 48 | #if canImport(_CryptoExtras) 49 | extension KDF.Insecure.PBKDF2.HashFunction { 50 | init(_: H.Type) throws where H: HashFunction { 51 | switch H.self { 52 | case is Insecure.SHA1.Type: 53 | self = .insecureSHA1 54 | case is SHA256.Type: 55 | self = .sha256 56 | case is SHA384.Type: 57 | self = .sha384 58 | case is SHA512.Type: 59 | self = .sha512 60 | default: 61 | throw CryptoKitError.incorrectKeySize 62 | } 63 | } 64 | } 65 | #endif 66 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Cryptography/Symmetric/SymmetricKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SymmetricKey.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/10/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | extension Crypto.SymmetricKey: Swift.Hashable, Swift.Decodable, Swift.Encodable {} 16 | 17 | extension SymmetricKey: JSONWebKeySymmetric { 18 | public var storage: JSONWebValueStorage { 19 | var result = AnyJSONWebKey() 20 | result.keyType = .symmetric 21 | result.keyValue = self 22 | return result.storage 23 | } 24 | 25 | /// Returns a new concrete key using json data. 26 | /// 27 | /// - Parameter storage: Storage of key-values. 28 | public init(storage: JSONWebValueStorage) throws { 29 | guard let data = AnyJSONWebKey(storage: storage).keyValue else { 30 | throw CryptoKitError.incorrectParameterSize 31 | } 32 | self.init(data: data) 33 | try validate() 34 | } 35 | 36 | public func hash(into hasher: inout Hasher) { 37 | withUnsafeBytes { 38 | hasher.combine(bytes: $0) 39 | } 40 | } 41 | } 42 | 43 | extension ContiguousBytes { 44 | @usableFromInline 45 | var data: Data { 46 | withUnsafeBytes { Data($0) } 47 | } 48 | } 49 | 50 | extension SymmetricKey { 51 | public var size: SymmetricKeySize { 52 | .init(bitCount: bitCount) 53 | } 54 | } 55 | 56 | extension Crypto.SymmetricKeySize: Swift.Hashable, Swift.Equatable { 57 | public static func == (lhs: Crypto.SymmetricKeySize, rhs: Crypto.SymmetricKeySize) -> Bool { 58 | lhs.bitCount == rhs.bitCount 59 | } 60 | 61 | public func hash(into hasher: inout Hasher) { 62 | hasher.combine(bitCount) 63 | } 64 | 65 | static func * (lhs: SymmetricKeySize, rhs: Int) -> SymmetricKeySize { 66 | .init(bitCount: lhs.bitCount * rhs) 67 | } 68 | } 69 | 70 | extension SymmetricKey: JSONWebSymmetricSigningKey { 71 | public init(algorithm: some JSONWebAlgorithm) throws { 72 | if let size = AnyJSONWebAlgorithm(algorithm).keyLength { 73 | self.init(size: .init(bitCount: size)) 74 | } else { 75 | self.init(size: .bits256) 76 | } 77 | } 78 | 79 | private func key(_ algorithm: JSONWebSignatureAlgorithm) throws -> (any JSONWebSymmetricSigningKey) { 80 | guard let keyClass = (algorithm.validatingKeyClass ?? (self.algorithm as? JSONWebSignatureAlgorithm)?.validatingKeyClass) as? any JSONWebSymmetricSigningKey.Type else { 81 | throw JSONWebKeyError.unknownAlgorithm 82 | } 83 | return try keyClass.init(self) 84 | } 85 | 86 | public func signature(_ data: D, using algorithm: JSONWebSignatureAlgorithm) throws -> Data where D: DataProtocol { 87 | try key(algorithm).signature(data, using: algorithm) 88 | } 89 | 90 | public func verifySignature(_ signature: S, for data: D, using algorithm: JSONWebSignatureAlgorithm) throws where S: DataProtocol, D: DataProtocol { 91 | try key(algorithm).verifySignature(signature, for: data, using: algorithm) 92 | } 93 | } 94 | 95 | extension SymmetricKey: JSONWebSymmetricDecryptingKey { 96 | private func key(_ algorithm: some JSONWebAlgorithm) throws -> (any JSONWebSymmetricDecryptingKey)? { 97 | if let keyClass = JSONWebKeyEncryptionAlgorithm(algorithm).decryptingKeyClass as? any JSONWebSymmetricDecryptingKey.Type { 98 | return try keyClass.init(self) 99 | } 100 | return nil 101 | } 102 | 103 | public func decrypt(_ data: D, using algorithm: JWA) throws -> Data where D: DataProtocol, JWA: JSONWebAlgorithm { 104 | if let key = try key(algorithm) { 105 | return try key.decrypt(data, using: algorithm) 106 | } 107 | 108 | switch algorithm { 109 | case .aesEncryptionGCM128, .aesEncryptionGCM192, .aesEncryptionGCM256: 110 | return try JSONWebKeyAESGCM(self).open(.init(combined: data, nonceLength: 12, tagLength: 16), using: algorithm) 111 | default: 112 | throw JSONWebKeyError.unknownAlgorithm 113 | } 114 | } 115 | 116 | public func encrypt(_ data: D, using algorithm: JWA) throws -> Data where D: DataProtocol, JWA: JSONWebAlgorithm { 117 | if let key = try key(algorithm) { 118 | return try key.encrypt(data, using: algorithm) 119 | } 120 | 121 | switch algorithm { 122 | case .aesEncryptionGCM128, .aesEncryptionGCM192, .aesEncryptionGCM256: 123 | return try JSONWebKeyAESGCM(self).seal(data, using: algorithm).combined 124 | default: 125 | throw JSONWebKeyError.unknownAlgorithm 126 | } 127 | } 128 | } 129 | 130 | extension SymmetricKey: JSONWebSymmetricSealingKey { 131 | public init(_ key: SymmetricKey) throws { 132 | self = key 133 | } 134 | 135 | private func key(_ algorithm: some JSONWebAlgorithm) throws -> any JSONWebSymmetricSealingKey { 136 | guard let keyClass = (algorithm as? JSONWebContentEncryptionAlgorithm)?.keyClass else { 137 | throw JSONWebKeyError.unknownAlgorithm 138 | } 139 | return try keyClass.init(self) 140 | } 141 | 142 | public func seal(_ data: D, iv: IV?, authenticating: AAD?, using algorithm: JWA) throws -> SealedData where D: DataProtocol, IV: DataProtocol, AAD: DataProtocol, JWA: JSONWebAlgorithm { 143 | try key(algorithm).seal(data, iv: iv, authenticating: authenticating, using: algorithm) 144 | } 145 | 146 | public func open(_ data: SealedData, authenticating: AAD?, using algorithm: JWA) throws -> Data where AAD: DataProtocol, JWA: JSONWebAlgorithm { 147 | try key(algorithm).open(data, authenticating: authenticating, using: algorithm) 148 | } 149 | } 150 | 151 | #if swift(<6.2) || !canImport(CryptoKit) 152 | extension SymmetricKey: @unchecked Swift.Sendable {} 153 | #endif 154 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Documentation.docc/Extensions/JWE.md: -------------------------------------------------------------------------------- 1 | # ``JWSETKit/JSONWebEncryption`` 2 | 3 | Use JWE to encrypt a payload. 4 | 5 | ## Overview 6 | 7 | JSON Web Encryption (JWE) represents encrypted content using JSON-based 8 | data structures RFC7159. The JWE cryptographic mechanisms encrypt 9 | and provide integrity protection for an arbitrary sequence of octets. 10 | 11 | ### Structure of a Compact JWE Token 12 | 13 | A JWE in compact serialization consists of five parts separated by dots (`.`): 14 | 15 | ``` 16 | header.encrypted_key.iv.ciphertext.tag 17 | ``` 18 | 19 | Each part is Base64URL encoded: 20 | 21 | 1. **Protected Header**: Contains metadata about encryption algorithms 22 | 2. **Encrypted Key**: Contains the content encryption key (CEK) encrypted with the recipient's key 23 | 3. **Initialization Vector**: Random data used as input to the encryption algorithm 24 | 4. **Ciphertext**: The encrypted payload data 25 | 5. **Authentication Tag**: Ensures data integrity and authenticity 26 | 27 | Visualized structure: 28 | ``` 29 | ┌─────────────┐ ┌─────────────┐ ┌─────┐ ┌───────────┐ ┌─────┐ 30 | │ Header │ │ Encrypted │ │ IV │ │Ciphertext │ │ Tag │ 31 | │ { │ │ Key │ │ │ │ │ │ │ 32 | │ "alg":"RSA1_5"│ (encrypted │ │ │ │ (encrypted │ │ │ 33 | │ "enc":"A256GCM"│ CEK) │ │ │ │ data) │ │ │ 34 | │ } │ │ │ │ │ │ │ │ │ 35 | └─────────────┘ └─────────────┘ └─────┘ └───────────┘ └─────┘ 36 | │ │ │ │ │ 37 | │ │ └──────────┼───────────┘ 38 | │ │ │ 39 | │ │ authenticated encryption 40 | │ │ │ 41 | │ key encryption │ 42 | │ │ │ 43 | └────────────────┼────────────────────────┘ 44 | │ 45 | recipient key 46 | ``` 47 | 48 | ## Decoding And Decrypting 49 | 50 | To create a new instance from compact or complete serialization, 51 | 52 | ``` swift 53 | do { 54 | let jwe = try JSONWebEncryption(from: jweString) 55 | // Work with the JWE object 56 | } catch { 57 | print("Failed to parse JWE: \(error)") 58 | } 59 | ``` 60 | 61 | Now it is possible to decrypt data using the appropriate key: 62 | 63 | ```swift 64 | do { 65 | let data = try jwe.decrypt(using: keyEncryptionKey) 66 | // Process the decrypted data 67 | } catch { 68 | print("Decryption failed: \(error)") 69 | } 70 | ``` 71 | 72 | Decrypted content now can be deserialized. For example if content is JWT claims: 73 | 74 | ```swift 75 | do { 76 | let claims = try JSONDecoder().decode(JSONWebTokenClaims.self, from: data) 77 | // Access the claims 78 | let subject = claims.subject 79 | } catch { 80 | print("Failed to decode claims: \(error)") 81 | } 82 | ``` 83 | 84 | ## Encrypting & Encoding 85 | 86 | To create a new container from plain-text data and a key, first 87 | create a new random *Key Encryption Key* or use an existing one. 88 | 89 | Either provide no `contentEncryptionKey` to generate new random one, 90 | or provide an existing one. 91 | 92 | Finally, serialize the result into string representation. 93 | 94 | ```swift 95 | // Generate a new AES-KeyWrap key with `A256KW` algorithm. 96 | let kek = JSONWebKeyAESKW(.bits256) 97 | 98 | // Alternatively, simply generate a 256-bit `SymmetricKey`. 99 | // 100 | // This works well as `keyEncryptingAlgorithm` is provided 101 | // in next step. 102 | let kek = SymmetricKey(size: .bits256) 103 | 104 | // Create JWE container with random content enc. key. 105 | let jwe = try! JSONWebEncryption( 106 | content: Data("Live long and prosper.".utf8), 107 | keyEncryptingAlgorithm: .aesKeyWrap256, 108 | keyEncryptionKey: kek, 109 | contentEncryptionAlgorithm: .aesEncryptionGCM128 110 | ) 111 | 112 | // Encode JWE in compact string. 113 | let jweString = try! String(jwe: jwe) 114 | ``` 115 | 116 | In case multiple recipient support is necessary or a unknown newly registered key type 117 | is used for encryption, you may first create encrypted key and sealed box and use 118 | ``JSONWebEncryption/init(header:recipients:sealed:additionalAuthenticatedData:)`` 119 | to create JWE instance from parts. 120 | 121 | ## Topics 122 | 123 | ### Contents 124 | 125 | - ``JSONWebEncryptionHeader`` 126 | - ``JSONWebEncryptionRecipient`` 127 | 128 | ### Key Encryption 129 | 130 | - ``JSONWebKeyEncryptionAlgorithm`` 131 | - ``JSONWebRSAPublicKey`` 132 | - ``JSONWebRSAPrivateKey`` 133 | - ``JSONWebKeyAESKW`` 134 | 135 | ### Content Encryption 136 | 137 | - ``JSONWebKeyAESGCM`` 138 | - ``JSONWebKeyAESCBCHMAC`` 139 | 140 | ### Encoding 141 | 142 | - ``JSONWebEncryptionRepresentation`` 143 | - ``JSONWebEncryptionCodableConfiguration`` 144 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Documentation.docc/Extensions/JWK.md: -------------------------------------------------------------------------------- 1 | # ``JWSETKit/JSONWebKey`` 2 | 3 | Working with JSON Web Keys (JWK) for cryptographic operations. 4 | 5 | ## Overview 6 | 7 | JSON Web Key (JWK) is a JSON data structure that represents a cryptographic key. The JWK format provides a standardized way to represent keys used for digital signatures, encryption, or other operations in a JSON-based format. 8 | 9 | ### Structure of a JWK 10 | 11 | A JWK is a JSON object containing information about a cryptographic key: 12 | 13 | ```json 14 | { 15 | "kty": "RSA", // Key Type (Required) 16 | "use": "sig", // Public Key Use (Optional) 17 | "kid": "1234", // Key ID (Optional) 18 | "alg": "RS256", // Algorithm (Optional) 19 | 20 | // Key-specific parameters (Required) 21 | // For RSA keys: 22 | "n": "base64url-encoded-modulus", 23 | "e": "base64url-encoded-exponent", 24 | 25 | // For EC keys: 26 | "crv": "P-256", 27 | "x": "base64url-encoded-x-coordinate", 28 | "y": "base64url-encoded-y-coordinate", 29 | 30 | // For symmetric keys: 31 | "k": "base64url-encoded-key-value" 32 | } 33 | ``` 34 | 35 | ### JWK Set (JWKS) 36 | 37 | Multiple JWKs can be grouped in a JWK Set (JWKS), which is a JSON object containing an array of keys: 38 | 39 | ```json 40 | { 41 | "keys": [ 42 | { 43 | "kty": "RSA", 44 | // ...other key parameters 45 | }, 46 | { 47 | "kty": "EC", 48 | // ...other key parameters 49 | } 50 | ] 51 | } 52 | ``` 53 | 54 | ## Working with JWKs 55 | 56 | ### Creating a JWK 57 | 58 | You can create a JWK from various sources: 59 | 60 | ```swift 61 | // Create a new RSA key pair 62 | let privateKey = try JSONWebRSAPrivateKey(keySize: .bits2048) 63 | let publicKey = privateKey.publicKey 64 | 65 | // Create a symmetric key for HMAC with SHA-256 66 | let hmacKey = try JSONWebKeyHMAC(.init(size: .bits256)) 67 | 68 | // Create an EC key pair - P-256 curve 69 | let ecPrivateKey = try JSONWebECPrivateKey(curve: .p256) 70 | 71 | // Using CryptoKit types directly 72 | // CryptoKit P-256 key 73 | let p256Key = P256.Signing.PrivateKey() 74 | let p256JWK = try JSONWebECPrivateKey(storage: p256Key.storage) 75 | 76 | // CryptoKit Symmetric key 77 | let symmetricKey = SymmetricKey(size: .bits256) 78 | let hmacJWK = try JSONWebKeyHMAC(symmetricKey) 79 | 80 | // CryptoKit Ed25519 key 81 | let edKey = Curve25519.Signing.PrivateKey() 82 | let edJWK = try JSONWebECPrivateKey(storage: edKey.storage) 83 | ``` 84 | 85 | ### Importing a JWK 86 | 87 | Import a JWK from various formats: 88 | 89 | ```swift 90 | // Import from JWK format (JSON) 91 | let jwkData = """ 92 | { 93 | "kty": "EC", 94 | "crv": "P-256", 95 | "x": "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 96 | "y": "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM" 97 | } 98 | """.data(using: .utf8)! 99 | 100 | let publicKey = try JSONWebECPublicKey(importing: jwkData, format: .jwk) 101 | 102 | // Import from DER/PEM format 103 | let derData = getKeyData() // Your DER-encoded key data 104 | let rsaKey = try JSONWebRSAPublicKey(importing: derData, format: .spki) 105 | 106 | // Import CryptoKit key from raw format 107 | let rawKeyData = Data(repeating: 1, count: 32) // 256-bit key 108 | let importedSymmetricKey = try SymmetricKey(data: rawKeyData) 109 | let importedHmacKey = try JSONWebKeyHMAC(importedSymmetricKey) 110 | ``` 111 | 112 | ### Exporting a JWK 113 | 114 | ```swift 115 | // Export to JWK format 116 | let jwkData = try publicKey.exportKey(format: .jwk) 117 | let jwkString = String(data: jwkData, encoding: .utf8) 118 | 119 | // Export to PKCS#8 or SPKI format 120 | let derData = try privateKey.exportKey(format: .pkcs8) 121 | 122 | // Export CryptoKit key 123 | let p256Key = P256.Signing.PrivateKey() 124 | let p256Data = try JSONWebECPrivateKey(storage: p256Key.storage).exportKey(format: .jwk) 125 | ``` 126 | 127 | ### Using a Key Set 128 | 129 | ```swift 130 | // Create a key set 131 | var keySet = JSONWebKeySet() 132 | keySet.append(publicKey) 133 | keySet.append(anotherKey) 134 | 135 | // Find keys by ID 136 | let foundKey = keySet.first { $0.keyId == "key-1" } 137 | 138 | // Extract public keys only 139 | let publicKeySet = keySet.publicKeyset 140 | 141 | // Export to JWKS format 142 | let encoder = JSONEncoder() 143 | let jwksData = try encoder.encode(keySet) 144 | ``` 145 | 146 | ## Key Thumbprints 147 | 148 | JWK thumbprints provide a unique identifier for a key: 149 | 150 | ```swift 151 | // Create a thumbprint 152 | let thumbprint = try publicKey.thumbprint(format: .jwk, using: SHA256.self) 153 | let thumbprintString = thumbprint.data.urlBase64EncodedString() 154 | 155 | // Create a thumbprint URI per RFC 9278 156 | let thumbprintUri = try publicKey.thumbprintUri(format: .jwk, using: SHA256.self) 157 | 158 | // Generate a key ID automatically using thumbprint 159 | var mutableKey = try JSONWebRSAPrivateKey(keySize: .bits2048) 160 | mutableKey.populateKeyIdIfNeeded() // Fills `kid` with thumbprint URI 161 | ``` 162 | 163 | ## Topics 164 | 165 | ### Key Types 166 | 167 | - ``JSONWebRSAPublicKey`` 168 | - ``JSONWebRSAPrivateKey`` 169 | - ``JSONWebECPublicKey`` 170 | - ``JSONWebECPrivateKey`` 171 | - ``JSONWebKeyHMAC`` 172 | - ``JSONWebKeyAESGCM`` 173 | - ``JSONWebKeyAESCBCHMAC`` 174 | - ``JSONWebKeyAESKW`` 175 | 176 | ### Key Containers 177 | 178 | - ``AnyJSONWebKey`` 179 | - ``JSONWebKeySet`` 180 | 181 | ### Key Operations 182 | 183 | - ``JSONWebValidatingKey`` 184 | - ``JSONWebSigningKey`` 185 | - ``JSONWebEncryptingKey`` 186 | - ``JSONWebDecryptingKey`` 187 | - ``JSONWebSealingKey`` 188 | 189 | ### Key Formats 190 | 191 | - ``JSONWebKeyFormat`` 192 | - ``JSONWebKeyType`` 193 | - ``JSONWebKeyCurve`` 194 | - ``JSONWebKeyUsage`` 195 | - ``JSONWebKeyOperation`` 196 | 197 | ### Key Import/Export 198 | 199 | - ``JSONWebKeyImportable`` 200 | - ``JSONWebKeyExportable`` -------------------------------------------------------------------------------- /Sources/JWSETKit/Documentation.docc/Extensions/JWSETKit.md: -------------------------------------------------------------------------------- 1 | # ``JWSETKit`` 2 | 3 | JWS / JWE / JWT Kit for Swift 4 | 5 | ## Overview 6 | 7 | JSON Web Signature (JWS) represents content secured with digital 8 | signatures or Message Authentication Codes (MACs) using JSON-based 9 | [RFC7159](https://datatracker.ietf.org/doc/html/rfc7159) data structures. 10 | The JWS cryptographic mechanisms provide integrity protection for 11 | an arbitrary sequence of octets. 12 | 13 | JSON Web Token (JWT) is a compact claims representation format 14 | intended for space constrained environments such as HTTP 15 | Authorization headers and URI query parameters. 16 | 17 | This module makes it possible to serialize, deserialize, create, 18 | and verify JWS/JWT messages. 19 | 20 | ## Getting Started 21 | 22 | To use JWSETKit, add the following dependency to your Package.swift: 23 | 24 | ```swift 25 | dependencies: [ 26 | .package(url: "https://github.com/amosavian/JWSETKit", .upToNextMinor(from: "0.24.0")) 27 | ] 28 | ``` 29 | 30 | Note that this repository does not have a 1.0 tag yet, so the API is not stable. 31 | 32 | You can then add the specific product dependency to your target: 33 | 34 | ```swift 35 | dependencies: [ 36 | .product(name: "JWSETKit", package: "JWSETKit"), 37 | ] 38 | ``` 39 | 40 | ## Usage 41 | 42 | ### JSON Web Token (JWT) 43 | 44 | Check ``JSONWebToken`` documentation for usage, validation and signing 45 | of JWT. 46 | 47 | ### JSON Web Signature (JWS) 48 | 49 | Check ``JSONWebSignature`` documentation for usage, validation and signing 50 | of JWS. 51 | 52 | ### JSON Web Encryption (JWE) 53 | 54 | Check ``JSONWebEncryption`` documentation for usage, encrypting and decrypting 55 | payload. 56 | 57 | ### JSON Web Key (JWK) 58 | 59 | Check ``JSONWebKey`` documentation for working with cryptographic keys in JWK format. 60 | 61 | ### JSON Web Algorithms (JWA) 62 | 63 | Check ``JSONWebAlgorithm`` documentation for information on supported algorithms 64 | and their usage. 65 | 66 | ## Topics 67 | 68 | ### Essentials 69 | 70 | - ``JSONWebToken`` 71 | - ``JSONWebSignature`` 72 | - ``JSONWebEncryption`` 73 | - ``JSONWebKey`` 74 | - ``JSONWebAlgorithm`` 75 | - 76 | 77 | ### Cryptography 78 | 79 | - 80 | - ``JSONWebKeySet`` 81 | - ``AnyJSONWebKey`` 82 | 83 | ### Algorithms 84 | 85 | - ``JSONWebSignatureAlgorithm`` 86 | - ``JSONWebKeyEncryptionAlgorithm`` 87 | - ``JSONWebContentEncryptionAlgorithm`` 88 | 89 | ### Extending 90 | 91 | - 92 | - ``JSONWebContainer`` 93 | - ``ProtectedWebContainer`` 94 | - ``TypedProtectedWebContainer`` 95 | - ``ProtectedJSONWebContainer`` 96 | - ``ProtectedDataWebContainer`` 97 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Documentation.docc/Extensions/JWT.md: -------------------------------------------------------------------------------- 1 | # ``JWSETKit/JSONWebToken`` 2 | 3 | Usage of JWT, Verifying and make new signatures. 4 | 5 | ## Overview 6 | 7 | JSON Web Token (JWT) is a compact, URL-safe means of representing 8 | claims to be transferred between two parties. 9 | 10 | The claims in a JWT are encoded as a JSON object that is used as 11 | the payload of a JSON Web Signature (JWS) structure or as the 12 | plaintext of a JSON Web Encryption (JWE) structure, enabling the 13 | claims to be digitally signed or integrity protected with a Message 14 | Authentication Code (MAC) and/or encrypted. 15 | 16 | ## Initializing And Encoding 17 | 18 | To create a JWT instance from `String` or `Data`: 19 | 20 | ```swift 21 | do { 22 | let jwt = try JSONWebToken(from: jwtString) 23 | // Work with the parsed JWT 24 | } catch { 25 | // Handle parsing errors 26 | print("Failed to parse JWT: \(error)") 27 | } 28 | ``` 29 | 30 | To assign a JWT to [`URLRequest`](https://developer.apple.com/documentation/foundation/urlrequest)'s 31 | `Authorization` header using ``Foundation/URLRequest/authorizationToken``: 32 | 33 | ```swift 34 | var request = URLRequest(url: URL(string: "https://www.example.com")!) 35 | request.authorizationToken = jwt 36 | ``` 37 | 38 | To convert a JWT instance back to its string representation: 39 | 40 | ```swift 41 | do { 42 | let jwtString = try String(jws: jwt) 43 | // Use jwtString 44 | } catch { 45 | print("Failed to serialize JWT: \(error)") 46 | } 47 | ``` 48 | 49 | Or more simply, using the `description` property (which returns an empty string if serialization fails): 50 | 51 | ```swift 52 | let jwtString = jwt.description 53 | ``` 54 | 55 | ## Accessing Claims 56 | 57 | Various claims, including registered and claims defined by [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) 58 | are predefined for JSON Web Token's payload. 59 | 60 | Claim names are more descriptive than keys defined by IANA Registry, 61 | for example `sub` claim became ``JSONWebTokenClaimsRegisteredParameters/subject`` 62 | and `iat` became ``JSONWebTokenClaimsRegisteredParameters/issuedAt``. 63 | 64 | For a complete list of predefined claims check ``JSONWebTokenClaimsRegisteredParameters``, 65 | ``JSONWebTokenClaimsOAuthParameters``, ``JSONWebTokenClaimsPublicOIDCAuthParameters`` and 66 | ``JSONWebTokenClaimsPublicOIDCStandardParameters``. 67 | 68 | For `StringORURL` types that are common to be a `URL`, there are two accessors 69 | for `String` and `URL`, e.g. 70 | ```swift 71 | let subjectString = jwt.subject // `sub` claim as String 72 | let subjectURL = jwt.subjectURL // `sub` claim parsed as URL 73 | ``` 74 | 75 | Date types are converted automatically from Unix Epoch to Swift's `Date`. 76 | 77 | For types that can be either a string or an array of strings, data type is `[String]`, 78 | ```swift 79 | let singleAudience = jwt.audience.first 80 | ``` 81 | 82 | Also ``JSONWebTokenClaimsOAuthParameters/scope`` items are separated by 83 | space according to standard and a list of items can be accessed 84 | using ``JSONWebTokenClaimsOAuthParameters/scopes``. 85 | 86 | ## Declaring New Claims 87 | 88 | To extend existing ``JSONWebTokenClaims`` define a new `struct` 89 | with proposed new claims and add a `JSONWebTokenClaims.subscript(dynamicMember:)` 90 | in order to access the claim. 91 | 92 | ```swift 93 | struct JSONWebTokenClaimsJwkParameters: JSONWebContainerParameters { 94 | var subJsonWebToken: (any JsonWebKey)? 95 | 96 | // Key lookup to convert claim to string key. 97 | static let keys: [SendablePartialKeyPath: String] = [ 98 | \.subJsonWebToken: "sub_jwk", 99 | ] 100 | } 101 | 102 | extension JSONWebTokenClaims { 103 | subscript(dynamicMember keyPath: SendableKeyPath) -> T? { 104 | get { 105 | storage[stringKey(keyPath)] 106 | } 107 | set { 108 | storage[stringKey(keyPath)] = newValue 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | Intrinsic supported types to be parsed by generic accessor are: 115 | 116 | - `UnsignedInteger` conforming types, e.g. `UInt`, `UInt32`, etc. 117 | - `SignedInteger` conforming types, e.g. `Int`, `Int32`, etc. 118 | - `BinaryFloatingPoint` conforming types, e.g. `Double`, `Float`, etc. 119 | - `Foundation.Decimal` 120 | - `Foundation.Date`, serialized as unix timestamp. 121 | - `Array`, `Foundation.Data`, `Foundation.NSData`, seriailzed as `Base64URL`. 122 | - `Foundation.URL` 123 | - `Foundation.Locale`, `Foundation.NSLocale` 124 | - `Foundation.TimeZone`, `Foundation.NSTimeZone` 125 | - Types conformed to ``JSONWebKey``. 126 | - Types conformed to ``JSONWebAlgorithm``. 127 | - Types conformed to `Foundation.Decodable`. 128 | 129 | 130 | ## Validating JWT 131 | 132 | A JSON Web Token has a signature that can be verified before using. 133 | Also `nbf` and `exp` claims can be verified using system date or a custom date. 134 | 135 | ### Verify Signature 136 | 137 | See [JWS Signature Verification](jsonwebsignature#Verify-Signature) 138 | 139 | ### Verify Expiration 140 | 141 | To verify that JWT is not expired yet, 142 | 143 | ```swift 144 | do { 145 | try jws.verifyDate() 146 | } catch let error as JSONWebValidationError { 147 | switch error { 148 | case .tokenExpired(let expiry): 149 | // Token is expired according to `exp` 150 | print(error.localizedDescription) 151 | await renewToken() 152 | case .tokenInvalidBefore(let notBefore): 153 | // Token is not valid yet according to `nbf` 154 | print(error.localizedDescription) 155 | case .audienceNotIntended(let audience): 156 | // Invalid audience. 157 | print(error.localizedDescription) 158 | } 159 | } catch { 160 | print(error.localizedDescription) 161 | } 162 | ``` 163 | 164 | A custom date can be passed to `verifyDate()`. 165 | 166 | ## Updating Signature 167 | 168 | See [JWS Signature Update](jsonwebsignature#UpdatingAdding-Signature) 169 | 170 | ## Topics 171 | 172 | ### JOSE Headers 173 | 174 | - ``JOSEHeader`` 175 | - ``JoseHeaderJWSRegisteredParameters`` 176 | 177 | ### JWT Claims 178 | 179 | - ``JSONWebTokenClaims`` 180 | - ``JSONWebTokenClaimsRegisteredParameters`` 181 | - ``JSONWebTokenClaimsOAuthParameters`` 182 | - ``JSONWebTokenClaimsPublicOIDCStandardParameters`` 183 | - ``JSONWebTokenClaimsPublicOIDCAuthParameters`` 184 | 185 | ### Signature 186 | 187 | - ``JSONWebSignature`` 188 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Documentation.docc/Manuals/5-SecurityGuidelines.md: -------------------------------------------------------------------------------- 1 | # Security Guidelines and Algorithm Selection 2 | 3 | Proper selection of cryptographic algorithms is critical for the security of your JWT/JWS/JWE implementations. This guide provides recommendations to help you make informed decisions. 4 | 5 | ## Algorithm Selection Guide 6 | 7 | ### Signature Algorithms 8 | 9 | | Algorithm | Security Level | Comments | Recommendation | 10 | |-----------|---------------|----------|----------------| 11 | | HS256 | Medium | Requires secure key management on both sides | Good for backend-to-backend communication | 12 | | HS384/HS512 | Higher | Increased resistance to brute force | Preferred over HS256 when possible | 13 | | RS256 | High | Most widely supported asymmetric algorithm | Good general choice for most applications | 14 | | RS384/RS512 | Higher | Increased resistance to attacks | Preferred over RS256 for sensitive data | 15 | | PS256/PS384/PS512 | High | More resistant to certain attacks than RS* | Preferred when clients support it | 16 | | ES256 | High | Faster and smaller signatures than RSA | Excellent for mobile applications | 17 | | ES384/ES512 | Higher | Increased resistance to quantum attacks | Recommended for long-term security | 18 | | EdDSA | Very High | Modern, fast, secure | Best choice when available | 19 | 20 | ### Key Encryption Algorithms 21 | 22 | | Algorithm | Security Level | Comments | Recommendation | 23 | |-----------|---------------|----------|----------------| 24 | | RSA1_5 | Low | Legacy, vulnerable to padding oracle attacks | Avoid if possible | 25 | | RSA-OAEP | Medium | Better than RSA1_5 | Acceptable | 26 | | RSA-OAEP-256 | High | Modern RSA padding | Recommended for RSA encryption | 27 | | A128KW/A192KW/A256KW | High | AES Key Wrap, secure | Good for symmetric key operations | 28 | | ECDH-ES | Very High | Forward secrecy | Excellent when supported | 29 | | PBES2-* | High | Password-based | Good for key derivation from passwords | 30 | 31 | ### Content Encryption Algorithms 32 | 33 | | Algorithm | Security Level | Comments | Recommendation | 34 | |-----------|---------------|----------|----------------| 35 | | A128CBC-HS256 | Medium | CBC mode requires careful implementation | Acceptable | 36 | | A256CBC-HS512 | High | Stronger variant | Better than A128CBC-HS256 | 37 | | A128GCM/A256GCM | Very High | Modern authenticated encryption | Recommended for most use cases | 38 | 39 | ## Best Practices 40 | 41 | ### Key Management 42 | 43 | 1. **Key Rotation**: Regularly rotate your signing keys to limit the impact of key compromise 44 | 2. **Key Size**: Use at least 2048 bits for RSA keys and 256 bits for elliptic curve keys 45 | 3. **Key Storage**: Store private keys securely using a hardware security module (HSM) or secure key vault 46 | 4. **Multiple Keys**: Maintain multiple keys to facilitate smooth key rotation 47 | 48 | ### JWT/JWS Implementation 49 | 50 | 1. **Token Expiry**: Always set reasonable expiration times via the `exp` claim 51 | 2. **Validate All Fields**: Verify all relevant claims and not just the signature 52 | 3. **Audience Validation**: Always validate the `aud` claim to prevent token reuse 53 | 4. **Algorithm Enforcement**: Explicitly check the `alg` header to prevent algorithm switching attacks 54 | 55 | ### Common Pitfalls to Avoid 56 | 57 | 1. **Algorithm Confusion**: Do not allow the algorithm to be switched from asymmetric to symmetric (e.g., from RS256 to HS256) 58 | 2. **None Algorithm**: Always reject tokens with the "none" algorithm 59 | 3. **Key Disclosure**: Never include sensitive key material in the token itself 60 | 4. **Signature Verification**: Always verify the signature before trusting any claims 61 | 62 | ## Algorithm Selection Decision Tree 63 | 64 | For selecting a suitable signature algorithm: 65 | 66 | 1. **If** client and server are trusted and can share secrets securely: 67 | - Use HMAC-based algorithms (HS256, HS384, HS512) 68 | 69 | 2. **If** you need public/private key separation: 70 | - **If** performance and token size are concerns: 71 | - Use Elliptic Curve algorithms (ES256, ES384, ES512, EdDSA) 72 | - **If** maximum compatibility is needed: 73 | - Use RSA-based algorithms (RS256, RS384, RS512) 74 | - **If** enhanced security is needed: 75 | - Use RSA-PSS algorithms (PS256, PS384, PS512) or EdDSA 76 | 77 | 3. **For** encryption: 78 | - **If** you need maximum security: 79 | - Use ECDH-ES for key management with A256GCM for content 80 | - **If** compatibility is a concern: 81 | - Use RSA-OAEP-256 for key management with A256CBC-HS512 for content 82 | 83 | ## References 84 | 85 | - [RFC 7518 - JSON Web Algorithms](https://www.rfc-editor.org/rfc/rfc7518.html) 86 | - [NIST Guidelines for Cryptographic Algorithm and Key Size Selection](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf) 87 | - [JWT Best Practices - RFC 8725](https://www.rfc-editor.org/rfc/rfc8725.html) 88 | 89 | ## Topics 90 | 91 | ### Security References 92 | 93 | - ``SECURITY`` 94 | - 95 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Documentation.docc/Manuals/7-Extending-Container.md: -------------------------------------------------------------------------------- 1 | # Extending Containers 2 | 3 | It is possible to create new containers or add new claims to existing containers. 4 | 5 | ## Add New Claims 6 | 7 | To extend existing container, e.g. ``JSONWebTokenClaims`` define a new `struct` 8 | with proposed new claims and add a `JSONWebTokenClaims.subscript(dynamicMember:)` 9 | in order to access the claim. 10 | 11 | ```swift 12 | struct JSONWebTokenClaimsJwkParameters: JSONWebContainerParameters { 13 | var subJsonWebToken: (any JsonWebKey)? 14 | 15 | // Key lookup to convert claim to string key. 16 | static let keys: [PartialKeyPath: String] = [ 17 | \.subJsonWebToken: "sub_jwk", 18 | ] 19 | } 20 | 21 | extension JSONWebTokenClaims { 22 | subscript(dynamicMember keyPath: SendableKeyPath) -> T? { 23 | get { 24 | storage[stringKey(keyPath)] 25 | } 26 | set { 27 | storage[stringKey(keyPath)] = newValue 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | ## Create New Container 34 | 35 | It is possible to create a completely new container for new purpose, e.g. to 36 | support [DPoP](https://datatracker.ietf.org/doc/html/rfc9449): 37 | 38 | ```swift 39 | struct DPoPClaims: JSONWebContainer { 40 | var storage: JSONWebValueStorage 41 | 42 | init(storage: JSONWebValueStorage) { 43 | self.storage = storage 44 | } 45 | 46 | static func create(storage: JSONWebValueStorage) throws -> JSONWebTokenClaims { 47 | .init(storage: storage) 48 | } 49 | } 50 | 51 | public typealias DPoP = JSONWebSignature> 52 | ``` 53 | 54 | then extend `DPoP` to support defined 55 | [claims](https://datatracker.ietf.org/doc/html/rfc9449#section-4.2) 56 | 57 | ``` swift 58 | public struct DPoPRegisteredParameters: JSONWebContainerParameters { 59 | public var jwtId: String? 60 | public var httpMethod: String? 61 | public var httpURL: URL? 62 | public var issuedAt: Date? 63 | public var accessTokenHash: Data? 64 | public var nonce: String? 65 | 66 | static let keys: [SendablePartialKeyPath: String] = [ 67 | \.jwtId: "jti", \.httpMethod: "htm", \.httpURL: "htu", 68 | \.issuedAt: "iat", \.accessTokenHash: "ath", \.nonce: "nonce", 69 | ] 70 | } 71 | 72 | extension DPoPClaims { 73 | @_documentation(visibility: private) 74 | public subscript(dynamicMember keyPath: SendableKeyPath) -> T? { 75 | get { 76 | storage[stringKey(keyPath)] 77 | } 78 | set { 79 | storage[stringKey(keyPath)] = newValue 80 | } 81 | } 82 | } 83 | ``` 84 | 85 | ## Topics 86 | 87 | ### JOSE Headers 88 | 89 | - ``JOSEHeader`` 90 | 91 | ### JSON Web Token 92 | 93 | - ``JSONWebToken`` 94 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Entities/JOSE/JOSE-JWEHPKERegistered.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JOSE-JWEHPKERegistered.swift 3 | // JWSETKit 4 | // 5 | // Created by Amir Abbas Mousavian on 1/31/25. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | /// Registered parameters of JOSE header in [Draft JOSE-HPKE](https://datatracker.ietf.org/doc/html/draft-ietf-jose-hpke-encrypt-11#section-10.2.1 ). 16 | public struct JoseHeaderJWEHPKERegisteredParameters: JSONWebContainerParameters { 17 | /// An encapsulated key as defined in [Section 5.1.1 of RFC9180](https://www.rfc-editor.org/rfc/rfc9180.html#section-5.1.1 ). 18 | public var encapsulatedKey: Data? 19 | 20 | /// A key identifier (`kid`) for the pre-shared key as defined in [Section 5.1.2 of RFC9180](https://www.rfc-editor.org/rfc/rfc9180.html#section-5.1.2 ). 21 | var presharedKeyId: Data? 22 | 23 | @_documentation(visibility: private) 24 | public static let keys: [SendablePartialKeyPath: String] = [ 25 | \.encapsulatedKey: "ek", \.presharedKeyId: "psk_id", 26 | ] 27 | } 28 | 29 | extension JOSEHeader { 30 | @_documentation(visibility: private) 31 | @inlinable 32 | public subscript(dynamicMember keyPath: SendableKeyPath) -> T? { 33 | get { 34 | storage[stringKey(keyPath)] 35 | } 36 | set { 37 | storage[stringKey(keyPath)] = newValue 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Entities/JOSE/JOSE-JWERegistered.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JOSE-JWERegistered.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/7/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | /// Registered parameters of JOSE header in [RFC 7516](https://www.rfc-editor.org/rfc/rfc7516.html ). 16 | public struct JoseHeaderJWERegisteredParameters: JSONWebContainerParameters { 17 | /// The "`enc`" (encryption algorithm) Header Parameter identifies 18 | /// the content encryption algorithm used to perform authenticated encryption 19 | /// on the plaintext to produce the ciphertext and the Authentication Tag. 20 | /// 21 | /// This algorithm MUST be an AEAD algorithm with a specified key length. 22 | /// 23 | /// The encrypted content is not usable if the "`enc`" value does not represent a supported algorithm. 24 | /// "`enc`" values should either be registered in 25 | /// the IANA "JSON Web Signature and Encryption Algorithms" registry established 26 | /// by [JWA] or be a value that contains a Collision-Resistant Name. 27 | /// 28 | /// The "enc" value is a case-sensitive ASCII string containing a `StringOrURI` value. 29 | /// 30 | /// This Header Parameter MUST be present and MUST be understood and processed by implementations. 31 | public var encryptionAlgorithm: JSONWebContentEncryptionAlgorithm? 32 | 33 | /// The "zip" (compression algorithm) applied to the plaintext before encryption, if any. 34 | /// 35 | /// The "zip" value defined by this specification is: 36 | /// - "DEF" - Compression with the DEFLATE [RFC1951] algorithm 37 | public var compressionAlgorithm: JSONWebCompressionAlgorithm? 38 | 39 | /// The "`epk`" (ephemeral public key) value created by the originator for 40 | /// the use in key agreement algorithms. 41 | /// 42 | /// This key is represented as a JSON Web Key (JWK) public key value. 43 | /// It MUST contain only public key parameters and SHOULD contain only 44 | /// the minimum JWK parameters necessary to represent the key; 45 | /// other JWK parameters included can be checked for consistency and honored, 46 | /// or they can be ignored. 47 | public var ephemeralPublicKey: AnyJSONWebKey? 48 | 49 | /// The "`apu`" (agreement PartyUInfo) value for key agreement algorithms 50 | /// using it (such as "`ECDH-ES`"), represented as a `base64url`-encoded string. 51 | /// 52 | /// When used, the PartyUInfo value contains information about 53 | /// the producer. Use of this Header Parameter is OPTIONAL. 54 | /// 55 | /// This Header Parameter MUST be understood and processed by 56 | /// implementations when these algorithms are used. 57 | public var agreementPartyUInfo: Data? 58 | 59 | /// The "`apv`" (agreement PartyVInfo) value for key agreement algorithms 60 | /// using it (such as "`ECDH-ES`"), represented as a `base64url` encoded string. 61 | /// 62 | /// When used, the PartyVInfo value contains information about 63 | /// the recipient. Use of this Header Parameter is OPTIONAL. 64 | /// 65 | /// This Header Parameter MUST be understood and processed by 66 | /// implementations when these algorithms are used. 67 | public var agreementPartyVInfo: Data? 68 | 69 | /// The "`iv`" (initialization vector) Header Parameter value is the 70 | /// `base64url-encoded` representation of the 96-bit IV value used for the 71 | /// key encryption operation. 72 | /// 73 | /// This Header Parameter MUST be present and MUST be understood and 74 | /// processed by implementations when these algorithms are used. 75 | public var initialVector: Data? 76 | 77 | /// The "`tag`" (authentication tag) Header Parameter value is the 78 | /// `base64url-encoded` representation of the 128-bit Authentication Tag 79 | /// value resulting from the key encryption operation. 80 | /// 81 | /// This Header Parameter MUST be present and MUST be understood and processed by 82 | /// implementations when these algorithms are used. 83 | public var authenticationTag: Data? 84 | 85 | /// The "`p2s`" (`PBES2` salt input) Header Parameter encodes a Salt Input 86 | /// value, which is used as part of the `PBKDF2` salt value. 87 | /// 88 | /// The "`p2s`" value is `BASE64URL(Salt Input)`. 89 | /// 90 | /// This Header Parameter MUST be present and MUST be understood 91 | /// and processed by implementations when these algorithms are used. 92 | /// 93 | /// A Salt Input value containing 8 or more octets MUST be used. 94 | /// A new Salt Input value MUST be generated randomly for every 95 | /// encryption operation; see RFC 4086 for considerations on 96 | /// generating random values. The salt value used is `(UTF8(Alg) || 0x00 || Salt Input)`, 97 | /// where Alg is the "`alg`" (algorithm) Header Parameter value. 98 | public var pbes2Salt: Data? 99 | 100 | /// The "`p2c`" (`PBES2` count) Header Parameter contains the `PBKDF2` 101 | /// iteration count, represented as a positive JSON integer. 102 | /// 103 | /// This Header Parameter MUST be present and MUST be understood and processed by 104 | /// implementations when these algorithms are used. 105 | /// 106 | /// The iteration count adds computational expense, ideally compounded by 107 | /// the possible range of keys introduced by the salt. A minimum 108 | /// iteration count of 1000 is RECOMMENDED. (600,000 by OWASP as of 2023) 109 | public var pbes2Count: Int? 110 | 111 | @_documentation(visibility: private) 112 | public static let keys: [SendablePartialKeyPath: String] = [ 113 | \.encryptionAlgorithm: "enc", \.compressionAlgorithm: "zip", 114 | \.ephemeralPublicKey: "epk", \.agreementPartyUInfo: "apu", \.agreementPartyVInfo: "apv", 115 | \.initialVector: "iv", \.authenticationTag: "tag", 116 | \.pbes2Salt: "p2s", \.pbes2Count: "p2c", 117 | ] 118 | } 119 | 120 | extension JOSEHeader { 121 | @_documentation(visibility: private) 122 | @inlinable 123 | public subscript(dynamicMember keyPath: SendableKeyPath) -> T? { 124 | get { 125 | storage[stringKey(keyPath)] 126 | } 127 | set { 128 | storage[stringKey(keyPath)] = newValue 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Entities/JWE/JWEHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWEHeader.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 10/3/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | /// Represents a signature or MAC over the JWS Payload and the JWS Protected Header. 16 | public struct JSONWebEncryptionHeader: Hashable, Sendable, Codable { 17 | enum CodingKeys: CodingKey { 18 | case protected 19 | case unprotected 20 | } 21 | 22 | /// JWE Protected Header. 23 | /// 24 | /// JSON object that contains the Header Parameters that are integrity 25 | /// protected by the authenticated encryption operation. 26 | /// 27 | /// These parameters apply to all recipients of the JWE. For the JWE 28 | /// Compact Serialization, this comprises the entire JOSE Header. For 29 | /// the JWE JSON Serialization, this is one component of the JOSE Header. 30 | public var protected: ProtectedJSONWebContainer 31 | 32 | /// JWE Shared Unprotected Header 33 | /// 34 | /// JSON object that contains the Header Parameters that apply to all 35 | /// recipients of the JWE that are not integrity protected. This can 36 | /// only be present when using the JWE JSON Serialization. 37 | public var unprotected: JOSEHeader? 38 | 39 | /// Creates a new JWR header using components. 40 | /// 41 | /// - Parameters: 42 | /// - header: JWE Protected Header. 43 | /// - unprotected: JWE Shared Unprotected Header. 44 | public init(protected: ProtectedJSONWebContainer, unprotected: JOSEHeader? = nil) throws { 45 | self.protected = protected 46 | self.unprotected = unprotected 47 | } 48 | 49 | /// Creates a new JWS header using components. 50 | /// 51 | /// - Parameters: 52 | /// - header: JWE Protected Header in byte array representation. 53 | /// - unprotected: JWE Shared Unprotected Header. 54 | public init(protected: Data, unprotected: JOSEHeader? = nil) throws { 55 | self.protected = try ProtectedJSONWebContainer(encoded: protected) 56 | self.unprotected = unprotected 57 | } 58 | 59 | public init(from decoder: any Decoder) throws { 60 | let container = try decoder.container(keyedBy: CodingKeys.self) 61 | 62 | self.protected = try container.decodeIfPresent(ProtectedJSONWebContainer.self, forKey: .protected) ?? .init(encoded: .init()) 63 | self.unprotected = try container.decodeIfPresent(JOSEHeader.self, forKey: .unprotected) 64 | } 65 | 66 | public func encode(to encoder: any Encoder) throws { 67 | try protected.validate() 68 | 69 | var container = encoder.container(keyedBy: CodingKeys.self) 70 | try container.encode(protected, forKey: .protected) 71 | try container.encodeIfPresent(unprotected, forKey: .unprotected) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Entities/JWE/JWERecipient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWERecipient.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 10/3/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | /// Contains JWE Per-Recipient Unprotected Header and 15 | /// content encryption key encrypted using recipient's public key. 16 | public struct JSONWebEncryptionRecipient: Hashable, Sendable, Codable { 17 | enum CodingKeys: String, CodingKey { 18 | case header 19 | case encryptedKey = "encrypted_key" 20 | } 21 | 22 | /// JWE Per-Recipient Unprotected Header. 23 | /// 24 | /// JSON object that contains Header Parameters that apply to a single 25 | /// recipient of the JWE. These Header Parameter values are not 26 | /// integrity protected. This can only be present when using the JWE JSON Serialization. 27 | public var header: JOSEHeader? 28 | 29 | /// Content Encryption Key (CEK). 30 | /// 31 | /// A symmetric key for the AEAD algorithm used to encrypt the 32 | /// plaintext to produce the ciphertext and the Authentication Tag. 33 | public var encryptedKey: Data 34 | 35 | /// Initializes a new recipient with given header and encrypted key. 36 | /// 37 | /// - Parameters: 38 | /// - header: JWE Per-Recipient Unprotected Header. 39 | /// - encryptedKey: Content Encryption Key (CEK). 40 | public init(header: JOSEHeader? = nil, encryptedKey: Data) { 41 | self.header = header 42 | self.encryptedKey = encryptedKey 43 | } 44 | 45 | public init(from decoder: any Decoder) throws { 46 | let container = try decoder.container(keyedBy: CodingKeys.self) 47 | 48 | self.header = try container.decodeIfPresent(JOSEHeader.self, forKey: JSONWebEncryptionRecipient.CodingKeys.header) 49 | let b64Key = try container.decode(String.self, forKey: JSONWebEncryptionRecipient.CodingKeys.encryptedKey) 50 | guard let key = Data(urlBase64Encoded: b64Key) else { 51 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath + [CodingKeys.encryptedKey], debugDescription: "Encrypted key is not a valid Base64URL")) 52 | } 53 | self.encryptedKey = key 54 | } 55 | 56 | public func encode(to encoder: any Encoder) throws { 57 | var container = encoder.container(keyedBy: CodingKeys.self) 58 | 59 | if let header, !header.storage.storageKeys.isEmpty { 60 | try container.encode(header, forKey: .header) 61 | } 62 | try container.encode(encryptedKey.urlBase64EncodedString(), forKey: .encryptedKey) 63 | } 64 | } 65 | 66 | extension [JSONWebEncryptionRecipient] { 67 | func match(for key: any JSONWebKey, keyId: String? = nil) throws -> Self.Element { 68 | if let keyId, let recipient = first(where: { 69 | $0.header?.keyId == keyId 70 | }) { 71 | return recipient 72 | } else if let recipient = first(where: { 73 | guard let algorithm = $0.header?.algorithm else { return false } 74 | return (algorithm.keyType == key.keyType && algorithm.curve == key.curve) || 75 | ($0.header?.ephemeralPublicKey?.keyType == key.keyType && $0.header?.ephemeralPublicKey?.curve == key.curve) 76 | }) { 77 | return recipient 78 | } else if let recipient = first { 79 | return recipient 80 | } else { 81 | throw JSONWebKeyError.keyNotFound 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Entities/JWS/JWSHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWSHeader.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/27/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import Crypto 14 | 15 | /// Represents a signature or MAC over the JWS Payload and the JWS Protected Header. 16 | public struct JSONWebSignatureHeader: Hashable, Codable, Sendable { 17 | enum CodingKeys: CodingKey { 18 | case protected 19 | case header 20 | case signature 21 | } 22 | 23 | /// For a JWS, the members of the JSON object(s) representing the JOSE Header 24 | /// describe the digital signature or MAC applied to the JWS Protected Header 25 | /// and the JWS Payload and optionally additional properties of the JWS. 26 | public var protected: ProtectedJSONWebContainer 27 | 28 | /// The value JWS Unprotected Header. 29 | public var unprotected: JOSEHeader? 30 | 31 | /// Signature of protected header concatenated with payload. 32 | public var signature: Data 33 | 34 | /// Combination of protected header and unprotected header. 35 | public var mergedHeader: JOSEHeader { 36 | protected.value.merging(unprotected ?? .init(), uniquingKeysWith: { protected, _ in protected }) 37 | } 38 | 39 | /// Creates a new JWS header using components. 40 | /// 41 | /// - Parameters: 42 | /// - protected: JWS Protected Header. 43 | /// - unprotected: JWS unsigned header. 44 | /// - signature: Signature of protected header concatenated with payload. 45 | public init(protected: JOSEHeader, unprotected: JOSEHeader? = nil, signature: Data) throws { 46 | self.protected = try .init(value: protected) 47 | self.unprotected = unprotected 48 | self.signature = signature 49 | } 50 | 51 | /// Creates a new JWS header using components. 52 | /// 53 | /// - Parameters: 54 | /// - protected: JWS Protected Header in byte array representation. 55 | /// - unprotected: JWS unsigned header. 56 | /// - signature: Signature of protected header concatenated with payload. 57 | public init(protected: Data, unprotected: JOSEHeader? = nil, signature: Data) throws { 58 | self.protected = try ProtectedJSONWebContainer(encoded: protected) 59 | self.unprotected = unprotected 60 | self.signature = signature 61 | } 62 | 63 | public init(from decoder: any Decoder) throws { 64 | let container = try decoder.container(keyedBy: CodingKeys.self) 65 | 66 | self.protected = try container.decodeIfPresent(ProtectedJSONWebContainer.self, forKey: .protected) ?? .init(value: .init()) 67 | self.unprotected = try container.decodeIfPresent(JOSEHeader.self, forKey: .header) 68 | 69 | let signatureString = try container.decodeIfPresent(String.self, forKey: .signature) ?? .init() 70 | self.signature = Data(urlBase64Encoded: signatureString) ?? .init() 71 | } 72 | 73 | public func encode(to encoder: any Encoder) throws { 74 | try protected.validate() 75 | 76 | var container = encoder.container(keyedBy: CodingKeys.self) 77 | try container.encode(protected, forKey: .protected) 78 | try container.encodeIfPresent(unprotected, forKey: .header) 79 | try container.encode(signature.urlBase64EncodedString(), forKey: .signature) 80 | } 81 | } 82 | 83 | extension JSONWebSignatureHeader { 84 | func signedData(_ payload: some ProtectedWebContainer) -> Data { 85 | let protectedEncoded = !protected.storage.storageKeys.isEmpty ? protected.encoded.urlBase64EncodedData() : .init() 86 | if protected.critical.contains("b64"), protected.base64 == false { 87 | return protectedEncoded + Data(".".utf8) + payload.encoded 88 | } else { 89 | return protectedEncoded + Data(".".utf8) + payload.encoded.urlBase64EncodedData() 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Entities/JWT/JWTOAuthClaims.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTOAuthClaims.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/7/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | /// Claims registered in [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693.html) 15 | public struct JSONWebTokenClaimsOAuthParameters: JSONWebContainerParameters { 16 | /// The authorization and token endpoints allow the client to specify the scope 17 | /// of the access request using the "scope" request parameter. 18 | /// 19 | /// In turn, the authorization server uses the "scope" response parameter 20 | /// to inform the client of the scope of the access token issued. 21 | public var scope: String? 22 | 23 | /// The authorization and token endpoints allow the client to specify the scope 24 | /// of the access request using the "scope" request parameter. 25 | /// 26 | /// In turn, the authorization server uses the "scope" response parameter 27 | /// to inform the client of the scope of the access token issued. 28 | public var scopes: [String] 29 | 30 | /// The authorization server issues the registered client a client identifier 31 | /// -- a unique string representing the registration information provided by the client. 32 | /// 33 | /// The client identifier is not a secret; it is exposed to the resource owner and MUST NOT 34 | /// be used alone for client authentication. 35 | /// The client identifier is unique to the authorization server. 36 | /// 37 | /// The client identifier string size is left undefined by this specification. 38 | /// The client should avoid making assumptions about the identifier size. 39 | /// The authorization server SHOULD document the size of any identifier it issues. 40 | public var clientID: String? 41 | 42 | @_documentation(visibility: private) 43 | public static let keys: [SendablePartialKeyPath: String] = [ 44 | \.scope: "scope", \.scopes: "scope", \.clientID: "client_id", 45 | ] 46 | } 47 | 48 | extension JSONWebTokenClaims { 49 | @_documentation(visibility: private) 50 | @inlinable 51 | public subscript(dynamicMember keyPath: SendableKeyPath) -> T? { 52 | get { 53 | storage[stringKey(keyPath)] 54 | } 55 | set { 56 | storage[stringKey(keyPath)] = newValue 57 | } 58 | } 59 | 60 | @_documentation(visibility: private) 61 | public subscript(dynamicMember keyPath: SendableKeyPath) -> [String] { 62 | get { 63 | (storage[stringKey(keyPath)] as String?)?.components(separatedBy: " ") ?? [] 64 | } 65 | set { 66 | storage[stringKey(keyPath)] = newValue.joined(separator: " ") 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Extensions/ASN1.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASN1.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/9/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | import SwiftASN1 14 | 15 | extension ASN1Node.Content { 16 | var primitive: ArraySlice? { 17 | switch self { 18 | case .constructed: 19 | return nil 20 | case .primitive(let value): 21 | return value 22 | } 23 | } 24 | 25 | var sequence: [ASN1Node]? { 26 | switch self { 27 | case .constructed(let nodes): 28 | return Array(nodes) 29 | case .primitive: 30 | return nil 31 | } 32 | } 33 | } 34 | 35 | extension DER.Serializer { 36 | mutating func appendIntegers(_ array: [Data]) throws { 37 | try appendConstructedNode(identifier: .sequence) { 38 | for item in array { 39 | try $0.serialize(ArraySlice(item)) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Extensions/Base64.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/5/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | extension RandomAccessCollection where Self.Element == UInt8 { 15 | /// Returns a URL-safe Base-64 encoded `Data`. 16 | /// 17 | /// - returns: The URL-safe Base-64 encoded data. 18 | public func urlBase64EncodedData() -> Data { 19 | let result = Data(self).base64EncodedData() 20 | .compactMap { (byte: UInt8) -> UInt8? in 21 | switch byte { 22 | case "+": 23 | return "-" 24 | case "/": 25 | return "_" 26 | case "=": 27 | return nil 28 | default: 29 | return byte 30 | } 31 | } 32 | return Data(result) 33 | } 34 | 35 | /// Returns a URL-safe Base-64 encoded `Data` in String representation. 36 | /// 37 | /// - returns: The URL-safe Base-64 encoded data in string representation. 38 | public func urlBase64EncodedString() -> String { 39 | String(decoding: urlBase64EncodedData(), as: UTF8.self) 40 | } 41 | } 42 | 43 | extension Swift.UInt8: Swift.ExpressibleByUnicodeScalarLiteral { 44 | public init(unicodeScalarLiteral value: UnicodeScalar) { 45 | self = .init(value.value) 46 | } 47 | } 48 | 49 | extension RandomAccessCollection where Self.Element == UInt8, Self: RangeReplaceableCollection { 50 | /// Initialize a `Data` from a URL-safe Base-64, UTF-8 encoded `Data`. 51 | /// 52 | /// Returns nil when the input is not recognized as valid Base-64. 53 | /// 54 | /// - parameter urlBase64Encoded: URL-safe Base-64, UTF-8 encoded input data. 55 | public init?(urlBase64Encoded: some Collection) { 56 | var base64Encoded = urlBase64Encoded.map { (byte: UInt8) -> UInt8 in 57 | switch byte { 58 | case "-": 59 | return "+" 60 | case "_": 61 | return "/" 62 | default: 63 | return byte 64 | } 65 | } 66 | if base64Encoded.count % 4 != 0 { 67 | base64Encoded.append(contentsOf: [UInt8](repeating: "=", count: 4 - base64Encoded.count % 4)) 68 | } 69 | guard let value = Data(base64Encoded: .init(base64Encoded), options: [.ignoreUnknownCharacters]) else { 70 | return nil 71 | } 72 | self.init(value) 73 | } 74 | 75 | /// Initialize a `Data` from a URL-safe Base-64 encoded String using the given options. 76 | /// 77 | /// Returns nil when the input is not recognized as valid Base-64. 78 | /// - parameter urlBase64Encoded: The string to parse. 79 | @inlinable 80 | public init?(urlBase64Encoded: some StringProtocol) { 81 | self.init(urlBase64Encoded: urlBase64Encoded.utf8) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Extensions/Codable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Codable.swift 3 | // JWSETKit 4 | // 5 | // Created by Amir Abbas Mousavian on 1/22/25. 6 | // 7 | 8 | @usableFromInline 9 | @frozen 10 | struct AnyCodable: Codable, @unchecked Sendable { 11 | let value: (any Sendable)? 12 | private var mirror: Mirror 13 | 14 | var codableValue: JSONWebValueStorage.Value? { 15 | guard let value else { 16 | return nil 17 | } 18 | if let val = value as? [JSONWebValueStorage.Key: (any Codable)?] { 19 | return (val as! JSONWebValueStorage.Value) 20 | } 21 | return ([value] as? [JSONWebValueStorage.Value])?[0] 22 | } 23 | 24 | @usableFromInline 25 | init(_ value: T?) { 26 | if let value = value as? AnyCodable { 27 | self = value 28 | } else { 29 | self.value = value 30 | self.mirror = value.customMirror 31 | } 32 | } 33 | 34 | @usableFromInline 35 | init(from decoder: any Decoder) throws { 36 | let container = try decoder.singleValueContainer() 37 | 38 | if container.decodeNil() { 39 | self.init(Self?.none) 40 | } else if let bool = try? container.decode(Bool.self) { 41 | self.init(bool) 42 | } else if let int = try? container.decode(Int.self) { 43 | self.init(int) 44 | } else if let uint = try? container.decode(UInt.self) { 45 | self.init(uint) 46 | } else if let double = try? container.decode(Double.self) { 47 | self.init(double) 48 | } else if let string = try? container.decode(String.self) { 49 | self.init(string) 50 | } else if let array = try? container.decode([AnyCodable].self) { 51 | self.init(array.map { $0.value }) 52 | } else if let dictionary = try? container.decode([String: AnyCodable].self) { 53 | self.init(dictionary.mapValues { $0.value }) 54 | } else { 55 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value cannot be decoded") 56 | } 57 | } 58 | 59 | @usableFromInline 60 | func encode(to encoder: any Encoder) throws { 61 | var container = encoder.singleValueContainer() 62 | 63 | switch value { 64 | case nil: 65 | try container.encodeNil() 66 | case let value as any JSONWebFieldEncodable: 67 | let codableValue = value.jsonWebValue as any Encodable 68 | try container.encode(codableValue) 69 | case let value as [(any Sendable)?]: 70 | try container.encode(value.map { AnyCodable($0) }) 71 | case let value as [String: (any Sendable)?]: 72 | try container.encode(value.mapValues { AnyCodable($0) }) 73 | case let value as any Encodable: 74 | try container.encode(value) 75 | default: 76 | let context = EncodingError.Context(codingPath: container.codingPath, debugDescription: "Value cannot be encoded") 77 | throw EncodingError.invalidValue(value as Any, context) 78 | } 79 | } 80 | } 81 | 82 | extension AnyCodable: CustomReflectable { 83 | @usableFromInline 84 | var customMirror: Mirror { 85 | mirror 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Extensions/Data.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 4/10/24. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | #if swift(<6.2) 15 | typealias SendableMetatype = Any 16 | #endif 17 | 18 | extension Data { 19 | init(value: T) where T: FixedWidthInteger { 20 | var int = value 21 | self.init(bytes: &int, count: MemoryLayout.size) 22 | } 23 | 24 | static func random(length: Int) -> Data { 25 | Data((0 ..< length).map { _ in UInt8.random(in: 0 ... 255) }) 26 | } 27 | } 28 | 29 | extension DataProtocol { 30 | @inlinable 31 | var asContiguousBytes: any ContiguousBytes { 32 | if regions.count == 1, let data = regions.first { 33 | return data 34 | } else { 35 | return Data(self) 36 | } 37 | } 38 | 39 | @inlinable 40 | func withUnsafeBuffer(_ body: (_ buffer: UnsafeRawBufferPointer) throws -> R) rethrows -> R { 41 | try withContiguousStorageIfAvailable { 42 | try body(UnsafeRawBufferPointer($0)) 43 | } ?? Data(self).withUnsafeBytes(body) 44 | } 45 | } 46 | 47 | infix operator =~=: ComparisonPrecedence 48 | 49 | @inlinable 50 | func =~= (_ lhs: LHS, _ rhs: RHS) -> Bool where LHS.Element == UInt8, RHS.Element == UInt8 { 51 | guard lhs.count == rhs.count else { 52 | return false 53 | } 54 | 55 | return zip(lhs, rhs).reduce(into: 0) { $0 |= $1.0 ^ $1.1 } == 0 56 | } 57 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Extensions/KeyLookup.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyLookup.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/7/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | #else 11 | import Foundation 12 | #endif 13 | 14 | @_documentation(visibility: private) 15 | public protocol JSONWebContainerParameters { 16 | static var keys: [SendablePartialKeyPath: String] { get } 17 | static var localizableKeys: [SendablePartialKeyPath] { get } 18 | } 19 | 20 | extension JSONWebContainerParameters { 21 | public static var localizableKeys: [SendablePartialKeyPath] { [] } 22 | } 23 | 24 | extension JSONWebContainer { 25 | @_documentation(visibility: private) 26 | public func stringKey(_ keyPath: SendableKeyPath) -> String { 27 | P.keys[keyPath] ?? keyPath.name.jsonWebKey 28 | } 29 | 30 | @_documentation(visibility: private) 31 | public func stringKey(_ keyPath: SendableKeyPath, force: Bool = false, locale: Locale) -> String { 32 | let key = P.keys[keyPath] ?? keyPath.name.jsonWebKey 33 | guard P.localizableKeys.contains(keyPath) else { return key } 34 | if force { 35 | return "\(key)#\(locale.bcp47)" 36 | } else { 37 | let locales = storage.storageKeys 38 | .filter { $0.hasPrefix(key + "#") } 39 | .map { $0.replacingOccurrences(of: key + "#", with: "", options: [.anchored]) } 40 | .map(Locale.init(identifier:)) 41 | guard let bestLocale = locale.bestMatch(in: locales) else { return key } 42 | return "\(key)#\(bestLocale.identifier)" 43 | } 44 | } 45 | } 46 | 47 | extension AnyKeyPath { 48 | var name: String { 49 | #if canImport(Darwin) || swift(>=6.0) 50 | // `components` never returns empty array. 51 | return String(String(reflecting: self).split(separator: ".").last!) 52 | #else 53 | assertionFailure("KeyPath reflection does not work correctly on non-Apple platforms.\n" + String(reflecting: self).components(separatedBy: ".").last!) 54 | return "" 55 | #endif 56 | } 57 | } 58 | 59 | extension String { 60 | #if canImport(Foundation.NSRegularExpression) 61 | // The pattern is valid and it never fails. 62 | private static let regex = (try? NSRegularExpression(pattern: "([a-z0-9])([A-Z])", options: [])).unsafelyUnwrapped 63 | #endif 64 | 65 | var snakeCased: String { 66 | #if canImport(Foundation.NSRegularExpression) 67 | let range = NSRange(startIndex..., in: self) 68 | return Self.regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: "$1_$2").lowercased() 69 | #else 70 | var result = "" 71 | for (index, char) in enumerated() { 72 | let lastIndex = index > 0 ? self.index(startIndex, offsetBy: index - 1) : startIndex 73 | if char.isUppercase, index > 0, !self[lastIndex].isUppercase { 74 | result.append("_") 75 | } 76 | result.append(char.lowercased()) 77 | } 78 | return result 79 | #endif 80 | } 81 | 82 | @usableFromInline 83 | var jsonWebKey: String { 84 | snakeCased 85 | .replacingOccurrences(of: "is_", with: "", options: [.anchored]) 86 | .replacingOccurrences(of: "_url", with: "", options: [.anchored, .backwards]) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Extensions/Localizing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Localizing.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/16/23. 6 | // 7 | 8 | #if canImport(FoundationEssentials) 9 | import FoundationEssentials 10 | 11 | extension String { 12 | init(localizingKey _: String, value: String, locale _: Locale) { 13 | self = value 14 | } 15 | 16 | init(localizingKey _: String, value: String, locale: Locale, _ arguments: any CVarArg...) { 17 | self.init(format: value, locale: locale, arguments: arguments) 18 | } 19 | 20 | init(localizingKey _: String, value: String, locale: Locale, arguments: [any CVarArg]) { 21 | self.init(format: value, locale: locale, arguments: arguments) 22 | } 23 | } 24 | #else 25 | import Foundation 26 | 27 | extension Bundle { 28 | func forLocale(_ locale: Locale) -> Bundle { 29 | if let url = urls(forResourcesWithExtension: "stringsdict", subdirectory: nil, localization: locale.identifier)?.first?.baseURL { 30 | return Bundle(url: url) ?? .module 31 | } else if let url = Bundle.module.urls(forResourcesWithExtension: "stringsdict", subdirectory: nil, localization: locale.languageIdentifier)?.first?.baseURL { 32 | return Bundle(url: url) ?? .module 33 | } 34 | return .module 35 | } 36 | } 37 | 38 | extension String { 39 | init(localizingKey key: String, value: String, locale: Locale) { 40 | let bundle: Bundle 41 | if locale != .autoupdatingCurrent, locale != .current { 42 | bundle = Bundle.module.forLocale(locale) 43 | } else { 44 | bundle = Bundle.module 45 | } 46 | self = bundle.localizedString(forKey: key, value: value, table: "") 47 | } 48 | 49 | init(localizingKey key: String, value: String, locale: Locale, _ arguments: any CVarArg...) { 50 | self = .init(format: .init(localizingKey: key, value: value, locale: locale), arguments: arguments) 51 | } 52 | 53 | init(localizingKey key: String, value: String, locale: Locale, arguments: [any CVarArg]) { 54 | self = .init(format: .init(localizingKey: key, value: value, locale: locale), arguments: arguments) 55 | } 56 | } 57 | #endif 58 | 59 | extension Locale { 60 | @inlinable 61 | var languageIdentifier: String? { 62 | #if canImport(Darwin) 63 | if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) { 64 | return language.languageCode?.identifier 65 | } else { 66 | return languageCode 67 | } 68 | #else 69 | return languageCode 70 | #endif 71 | } 72 | 73 | @inlinable 74 | var countryCode: String? { 75 | #if canImport(Darwin) 76 | if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) { 77 | return region?.identifier 78 | } else { 79 | return regionCode 80 | } 81 | #else 82 | return regionCode 83 | #endif 84 | } 85 | 86 | @inlinable 87 | var writeScript: String? { 88 | #if canImport(Darwin) 89 | if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) { 90 | return language.script?.identifier 91 | } else { 92 | return scriptCode 93 | } 94 | #else 95 | return scriptCode 96 | #endif 97 | } 98 | 99 | var bcp47: String { 100 | #if canImport(Darwin) 101 | if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) { 102 | return identifier(.bcp47) 103 | } else { 104 | return identifier.replacingOccurrences(of: "_", with: "-") 105 | } 106 | #else 107 | return identifier.replacingOccurrences(of: "_", with: "-") 108 | #endif 109 | } 110 | 111 | init(bcp47: String) { 112 | #if canImport(Darwin) 113 | if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) { 114 | self.init(components: .init(identifier: bcp47)) 115 | } else { 116 | self.init(identifier: bcp47.replacingOccurrences(of: "-", with: "_")) 117 | } 118 | #else 119 | self.init(identifier: bcp47.replacingOccurrences(of: "-", with: "_")) 120 | #endif 121 | } 122 | 123 | func bestMatch(in locales: [Locale]) -> Locale? { 124 | guard !locales.isEmpty, let languageIdentifier = languageIdentifier else { return nil } 125 | let matchedLanguages = locales.filter { $0.languageIdentifier == languageIdentifier } 126 | switch matchedLanguages.count { 127 | case 0: 128 | return nil 129 | case 1: 130 | return matchedLanguages[0] 131 | default: 132 | break 133 | } 134 | let matchedScript: [Locale] 135 | if let writeScript = writeScript { 136 | matchedScript = matchedLanguages.filter { $0.writeScript == writeScript } 137 | switch matchedScript.count { 138 | case 0: 139 | return matchedLanguages[0] 140 | case 1: 141 | return matchedScript[0] 142 | default: 143 | break 144 | } 145 | } else { 146 | matchedScript = matchedLanguages 147 | } 148 | if let countryCode = countryCode { 149 | let matchedCountry = matchedScript.filter { $0.countryCode == countryCode } 150 | switch matchedCountry.count { 151 | case 0: 152 | return matchedScript.first { $0.countryCode == nil } ?? matchedScript[0] 153 | default: 154 | return matchedCountry[0] 155 | } 156 | } else { 157 | return matchedScript[0] 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/JWSETKit/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Resources/ar.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | errorUnknownAlgorithm 6 | 7 | NSStringLocalizedFormatKey 8 | الخوارزمية المعطاة للتوقيع / التشفير غير مدعومة. 9 | 10 | errorUnknownKeyType 11 | 12 | NSStringLocalizedFormatKey 13 | نوع المفتاح غير مدعوم. 14 | 15 | errorDecryptionFailed 16 | 17 | NSStringLocalizedFormatKey 18 | فك تشفير النص الرمزي باستخدام المفتاح المعطى غير ممكن. 19 | 20 | errorKeyNotFound 21 | 22 | NSStringLocalizedFormatKey 23 | فشل في العثور على المفتاح المعطى. 24 | 25 | errorOperationNotAllowed 26 | 27 | NSStringLocalizedFormatKey 28 | العملية غير مسموح بها. 29 | 30 | errorInvalidKeyFormat 31 | 32 | NSStringLocalizedFormatKey 33 | تنسيق المفتاح غير صالح 34 | 35 | errorExpiredToken 36 | 37 | NSStringLocalizedFormatKey 38 | الرمز غير صالح بعد %@ 39 | 40 | notBeforeToken 41 | 42 | NSStringLocalizedFormatKey 43 | الرمز غير صالح قبل %@ 44 | 45 | errorInvalidAudience 46 | 47 | NSStringLocalizedFormatKey 48 | الجمهور "%@" ليس مقصودًا للرمز المميز. 49 | 50 | errorMissingField 51 | 52 | NSStringLocalizedFormatKey 53 | الحقل المطلوب "%@" مفقود. 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Resources/en.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | errorUnknownAlgorithm 6 | 7 | NSStringLocalizedFormatKey 8 | Given signature/encryption algorithm is no supported. 9 | 10 | errorUnknownKeyType 11 | 12 | NSStringLocalizedFormatKey 13 | Key type is not supported. 14 | 15 | errorDecryptionFailed 16 | 17 | NSStringLocalizedFormatKey 18 | Decrypting cipher-text using given key is not possible. 19 | 20 | errorKeyNotFound 21 | 22 | NSStringLocalizedFormatKey 23 | Failed to find given key. 24 | 25 | errorOperationNotAllowed 26 | 27 | NSStringLocalizedFormatKey 28 | Operation Not Allowed. 29 | 30 | errorInvalidKeyFormat 31 | 32 | NSStringLocalizedFormatKey 33 | Invalid Key Format 34 | 35 | errorExpiredToken 36 | 37 | NSStringLocalizedFormatKey 38 | Token is invalid after %@ 39 | 40 | notBeforeToken 41 | 42 | NSStringLocalizedFormatKey 43 | Token is invalid before %@ 44 | 45 | errorInvalidAudience 46 | 47 | NSStringLocalizedFormatKey 48 | Audience "%@" is not intended for the token. 49 | 50 | errorMissingField 51 | 52 | NSStringLocalizedFormatKey 53 | Required "%@" field is missing. 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Resources/es.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | errorUnknownAlgorithm 6 | 7 | NSStringLocalizedFormatKey 8 | El algoritmo de firma/cifrado dado no es compatible. 9 | 10 | errorUnknownKeyType 11 | 12 | NSStringLocalizedFormatKey 13 | El tipo de clave no es compatible. 14 | 15 | errorDecryptionFailed 16 | 17 | NSStringLocalizedFormatKey 18 | No es posible descifrar el texto cifrado con la clave proporcionada. 19 | 20 | errorKeyNotFound 21 | 22 | NSStringLocalizedFormatKey 23 | No se pudo encontrar la clave proporcionada. 24 | 25 | errorOperationNotAllowed 26 | 27 | NSStringLocalizedFormatKey 28 | Operación no permitida. 29 | 30 | errorInvalidKeyFormat 31 | 32 | NSStringLocalizedFormatKey 33 | Formato de clave inválido. 34 | 35 | errorExpiredToken 36 | 37 | NSStringLocalizedFormatKey 38 | El token es inválido después de %@ 39 | 40 | notBeforeToken 41 | 42 | NSStringLocalizedFormatKey 43 | El token es inválido antes de %@ 44 | 45 | errorInvalidAudience 46 | 47 | NSStringLocalizedFormatKey 48 | La audiencia "%@" no está destinada para el token. 49 | 50 | errorMissingField 51 | 52 | NSStringLocalizedFormatKey 53 | Falta el campo requerido "%@". 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Resources/fa.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | errorUnknownAlgorithm 6 | 7 | NSStringLocalizedFormatKey 8 | الگوریتم انتخابی برای امضا/رمز پشتیبانی نمی‌شود. 9 | 10 | errorUnknownKeyType 11 | 12 | NSStringLocalizedFormatKey 13 | نوع کلید پشتیبانی نمی‌شود. 14 | 15 | errorDecryptionFailed 16 | 17 | NSStringLocalizedFormatKey 18 | بازگشایی رمز با کلید داده شده امکان پذیر نیست. 19 | 20 | errorKeyNotFound 21 | 22 | NSStringLocalizedFormatKey 23 | کلید مورد نظر یافت نشد. 24 | 25 | errorOperationNotAllowed 26 | 27 | NSStringLocalizedFormatKey 28 | عملیات پشتیبانی نمی‌شود. 29 | 30 | errorInvalidKeyFormat 31 | 32 | NSStringLocalizedFormatKey 33 | فرمت کلید نامعنبر است. 34 | 35 | errorExpiredToken 36 | 37 | NSStringLocalizedFormatKey 38 | توکن برای پس از %@ نامعتبر است. 39 | 40 | notBeforeToken 41 | 42 | NSStringLocalizedFormatKey 43 | توکن برای قبل از %@ نامعتبر است. 44 | 45 | errorInvalidAudience 46 | 47 | NSStringLocalizedFormatKey 48 | مخاطب %@ برای این توکن نامعتبر است. 49 | 50 | errorMissingField 51 | 52 | NSStringLocalizedFormatKey 53 | فیلد ضروری "%@" وجود ندارد. 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Sources/JWSETKit/Resources/fr.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | errorUnknownAlgorithm 6 | 7 | NSStringLocalizedFormatKey 8 | L’algorithme de signature/chiffrement donné n’est pas pris en charge. 9 | 10 | errorUnknownKeyType 11 | 12 | NSStringLocalizedFormatKey 13 | Key type is not supported. 14 | 15 | errorDecryptionFailed 16 | 17 | NSStringLocalizedFormatKey 18 | Il n’est pas possible de déchiffrer le texte chiffré avec la clé donnée. 19 | 20 | errorKeyNotFound 21 | 22 | NSStringLocalizedFormatKey 23 | Échec de la recherche de la clé donnée 24 | 25 | errorOperationNotAllowed 26 | 27 | NSStringLocalizedFormatKey 28 | Opération non autorisée. 29 | 30 | errorInvalidKeyFormat 31 | 32 | NSStringLocalizedFormatKey 33 | Format de clé invalide. 34 | 35 | errorExpiredToken 36 | 37 | NSStringLocalizedFormatKey 38 | Le jeton est invalide après %@ 39 | 40 | notBeforeToken 41 | 42 | NSStringLocalizedFormatKey 43 | Le jeton est invalide avant %@ 44 | 45 | errorInvalidAudience 46 | 47 | NSStringLocalizedFormatKey 48 | L’audience "%@" n’est pas destinée au jeton. 49 | 50 | errorMissingField 51 | 52 | NSStringLocalizedFormatKey 53 | Le champ requis "%@" est manquant. 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Base/StorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/16/23. 6 | // 7 | 8 | import Crypto 9 | import Foundation 10 | import Testing 11 | @testable import JWSETKit 12 | 13 | @Suite 14 | struct StorageTests { 15 | let testValue: String = """ 16 | { 17 | "iss":"joe", 18 | "exp":1300819380, 19 | "http://example.com/is_root":true, 20 | "website": "https://www.apple.com/", 21 | "e":"AQAB==", 22 | "eu":"AQAB", 23 | "keys": [ 24 | { 25 | "kty":"EC", 26 | "crv":"P-256", 27 | "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 28 | "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM" 29 | }, 30 | { 31 | "kty":"RSA", 32 | "n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ", 33 | "e":"AQAB", 34 | "d":"Eq5xpGnNCivDflJsRQBXHx1hdR1k6Ulwe2JZD50LpXyWPEAeP88vLNO97IjlA7_GQ5sLKMgvfTeXZx9SE-7YwVol2NXOoAJe46sui395IW_GO-pWJ1O0BkTGoVEn2bKVRUCgu-GjBVaYLU6f3l9kJfFNS3E0QbVdxzubSu3Mkqzjkn439X0M_V51gfpRLI9JYanrC4D4qAdGcopV_0ZHHzQlBjudU2QvXt4ehNYTCBr6XCLQUShb1juUO1ZdiYoFaFQT5Tw8bGUl_x_jTj3ccPDVZFD9pIuhLhBOneufuBiB4cS98l2SR_RQyGWSeWjnczT0QU91p1DhOVRuOopznQ", 35 | "p":"4BzEEOtIpmVdVEZNCqS7baC4crd0pqnRH_5IB3jw3bcxGn6QLvnEtfdUdiYrqBdss1l58BQ3KhooKeQTa9AB0Hw_Py5PJdTJNPY8cQn7ouZ2KKDcmnPGBY5t7yLc1QlQ5xHdwW1VhvKn-nXqhJTBgIPgtldC-KDV5z-y2XDwGUc", 36 | "q":"uQPEfgmVtjL0Uyyx88GZFF1fOunH3-7cepKmtH4pxhtCoHqpWmT8YAmZxaewHgHAjLYsp1ZSe7zFYHj7C6ul7TjeLQeZD_YwD66t62wDmpe_HlB-TnBA-njbglfIsRLtXlnDzQkv5dTltRJ11BKBBypeeF6689rjcJIDEz9RWdc", 37 | "dp":"BwKfV3Akq5_MFZDFZCnW-wzl-CCo83WoZvnLQwCTeDv8uzluRSnm71I3QCLdhrqE2e9YkxvuxdBfpT_PI7Yz-FOKnu1R6HsJeDCjn12Sk3vmAktV2zb34MCdy7cpdTh_YVr7tss2u6vneTwrA86rZtu5Mbr1C1XsmvkxHQAdYo0", 38 | "dq":"h_96-mK1R_7glhsum81dZxjTnYynPbZpHziZjeeHcXYsXaaMwkOlODsWa7I9xXDoRwbKgB719rrmI2oKr6N3Do9U0ajaHF-NKJnwgjMd2w9cjz3_-kyNlxAr2v4IKhGNpmM5iIgOS1VZnOZ68m6_pbLBSp3nssTdlqvd0tIiTHU", 39 | "qi":"IYd7DHOhrWvxkwPQsRM2tOgrjbcrfvtQJipd-DlcxyVuuM9sQLdgjVk2oy26F0EmpScGLq2MowX7fhd_QJQ3ydy5cY7YIBi87w93IKLEdfnbJtoOPLUW0ITrJReOgo1cq9SbsxYawBgfp_gh6A5603k2-ZQwVK0JKSHuLFkuQ3U" 40 | } 41 | ] 42 | } 43 | """ 44 | 45 | @Test 46 | func accessors() throws { 47 | let storage = try JSONDecoder().decode(JSONWebValueStorage.self, from: .init(testValue.utf8)) 48 | 49 | #expect(storage.iss == "joe") 50 | #expect(storage.exp == Date(timeIntervalSince1970: 1_300_819_380)) 51 | #expect(storage["http://example.com/is_root"] == true) 52 | #expect(storage["website"] == "https://www.apple.com/") 53 | 54 | let keys = (storage.keys as [AnyJSONWebKey]).map { $0.specialized() } 55 | try #require(keys.count == 2) 56 | #expect(keys[0].keyType == .ellipticCurve) 57 | #expect( 58 | try P256.Signing.PublicKey(from: keys[0]).rawRepresentation 59 | == "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D7gS2XpJFbZiItSs3m9+9Ue6GnvHw/GW2ZZaVtszggXIw==".decoded 60 | ) 61 | #expect(keys[1].keyType == .rsa) 62 | #expect(type(of: keys[0]) == JSONWebECPublicKey.self) 63 | #expect(type(of: keys[1]) == JSONWebRSAPrivateKey.self) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Base/WebContainerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WebContainerTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/16/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct WebContainerTests { 14 | let protected = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" 15 | 16 | @Test 17 | func testProtected() throws { 18 | let container = try ProtectedJSONWebContainer(encoded: .init( 19 | urlBase64Encoded: protected)!) 20 | #expect(container.value.issuer == "joe") 21 | #expect(container.value.expiry == Date(timeIntervalSince1970: 1_300_819_380)) 22 | #expect(container.value["http://example.com/is_root"] == true) 23 | #expect(container.encoded == .init(urlBase64Encoded: protected)!) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Cryptography/CompressionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompressionTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 11/24/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct CompressionTests { 14 | let decompressed = "Data compression test. This text must be compressed.".data 15 | let deflateCompressed = "c0ksSVRIzs8tKEotLs7Mz1MoSS0u0VMIycgsBjIrShRyS4tLFJJS4WpSU/QA".decoded 16 | 17 | var compressors: [any JSONWebCompressor.Type] 18 | 19 | init() { 20 | var compressors: [any JSONWebCompressor.Type] = [] 21 | #if canImport(Compression) 22 | compressors.append(AppleCompressor.self) 23 | #endif 24 | #if canImport(Czlib) || canImport(zlib) 25 | compressors.append(ZlibCompressor.self) 26 | #endif 27 | self.compressors = compressors 28 | } 29 | 30 | @Test 31 | func deflateCompression() throws { 32 | for deflateCompressor in compressors { 33 | let testCompressed = try deflateCompressor.compress(decompressed) 34 | #expect(testCompressed == deflateCompressed) 35 | #expect(testCompressed.count < decompressed.count) 36 | } 37 | } 38 | 39 | @Test 40 | func deflateDecompression() throws { 41 | for deflateCompressor in compressors { 42 | let testDecompressed = try deflateCompressor.decompress(deflateCompressed) 43 | #expect(testDecompressed == decompressed) 44 | } 45 | } 46 | 47 | @Test 48 | func compressionDecompression() throws { 49 | let length = Int.random(in: (1 << 17) ... (1 << 20)) // 128KB to 1MB 50 | let random = Data.random(length: length) 51 | .urlBase64EncodedData() 52 | for algorithm in JSONWebCompressionAlgorithm.registeredAlgorithms { 53 | guard let compressor = algorithm.compressor else { continue } 54 | let testCompressed = try compressor.compress(random) 55 | let testDecompressed = try compressor.decompress(testCompressed) 56 | #expect(testCompressed.count < random.count) 57 | #expect(Data(random) == testDecompressed) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Cryptography/ECTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ECTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 12/27/23. 6 | // 7 | 8 | import Crypto 9 | import Foundation 10 | import Testing 11 | @testable import JWSETKit 12 | 13 | @Suite 14 | struct ECTests { 15 | let plaintext = Data("The quick brown fox jumps over the lazy dog.".utf8) 16 | 17 | @Test 18 | func p256() throws { 19 | let key = P256.Signing.PrivateKey() 20 | 21 | #expect(key.xCoordinate != nil) 22 | #expect(key.yCoordinate != nil) 23 | #expect(key.privateKey != nil) 24 | 25 | let signature = try key.signature(plaintext, using: .ecdsaSignatureP256SHA256) 26 | try key.verifySignature(signature, for: plaintext, using: .ecdsaSignatureP256SHA256) 27 | } 28 | 29 | @Test 30 | func p256Decode() throws { 31 | let signature = try ExampleKeys.privateEC256.signature(plaintext, using: .ecdsaSignatureP256SHA256) 32 | try ExampleKeys.publicEC256.verifySignature(signature, for: plaintext, using: .ecdsaSignatureP256SHA256) 33 | } 34 | 35 | @Test 36 | func p384() throws { 37 | let key = P384.Signing.PrivateKey() 38 | 39 | #expect(key.xCoordinate != nil) 40 | #expect(key.yCoordinate != nil) 41 | #expect(key.privateKey != nil) 42 | 43 | let signature = try key.signature(plaintext, using: .ecdsaSignatureP384SHA384) 44 | try key.verifySignature(signature, for: plaintext, using: .ecdsaSignatureP384SHA384) 45 | } 46 | 47 | @Test 48 | func p384Decode() throws { 49 | let signature = try ExampleKeys.privateEC384.signature(plaintext, using: .ecdsaSignatureP384SHA384) 50 | try ExampleKeys.publicEC384.verifySignature(signature, for: plaintext, using: .ecdsaSignatureP384SHA384) 51 | } 52 | 53 | @Test 54 | func p521() throws { 55 | let key = P521.Signing.PrivateKey() 56 | 57 | #expect(key.xCoordinate != nil) 58 | #expect(key.yCoordinate != nil) 59 | #expect(key.privateKey != nil) 60 | 61 | let signature = try key.signature(plaintext, using: .ecdsaSignatureP521SHA512) 62 | try key.verifySignature(signature, for: plaintext, using: .ecdsaSignatureP521SHA512) 63 | } 64 | 65 | @Test 66 | func p521Decode() throws { 67 | let signature = try ExampleKeys.privateEC521.signature(plaintext, using: .ecdsaSignatureP521SHA512) 68 | try ExampleKeys.publicEC521.verifySignature(signature, for: plaintext, using: .ecdsaSignatureP521SHA512) 69 | } 70 | 71 | @Test 72 | func eddsa() throws { 73 | let key = Curve25519.Signing.PrivateKey() 74 | 75 | #expect(key.xCoordinate != nil) 76 | #expect(key.yCoordinate == nil) 77 | #expect(key.privateKey != nil) 78 | 79 | let signature = try key.signature(plaintext, using: .eddsaSignature) 80 | try key.verifySignature(signature, for: plaintext, using: .eddsaSignature) 81 | } 82 | 83 | @Test 84 | func eddsaDecode() throws { 85 | let key = ExampleKeys.privateEd25519 86 | 87 | #expect(key.keyId != nil) 88 | #expect(key.xCoordinate != nil) 89 | #expect(key.yCoordinate == nil) 90 | #expect(key.privateKey != nil) 91 | 92 | let signature = try ExampleKeys.privateEd25519.signature(plaintext, using: .eddsaSignature) 93 | try ExampleKeys.publicEd25519.verifySignature(signature, for: plaintext, using: .eddsaSignature) 94 | } 95 | 96 | @Test 97 | func eddsaPublicDecodeSPKI() throws { 98 | let key = "MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE=".decoded 99 | #expect(try JSONWebECPublicKey(importing: key, format: .spki).keyType == .octetKeyPair) 100 | #expect(throws: Never.self) { 101 | try Curve25519.Signing.PublicKey(importing: key, format: .spki) 102 | } 103 | } 104 | 105 | @Test 106 | func eddsaPrivateDecodePKCS8() throws { 107 | let key = "MC4CAQAwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC".decoded 108 | #expect(try JSONWebECPrivateKey(importing: key, format: .pkcs8).keyType == .octetKeyPair) 109 | #expect(throws: Never.self) { 110 | try Curve25519.Signing.PrivateKey(importing: key, format: .pkcs8) 111 | } 112 | } 113 | 114 | @Test 115 | func eddsaPrivateDecodePKCS8v2() throws { 116 | let key = """ 117 | MHICAQEwBQYDK2VwBCIEINTuctv5E1hK1bbY8fdp+K06/nwoy/HU++CXqI9EdVhC\ 118 | oB8wHQYKKoZIhvcNAQkJFDEPDA1DdXJkbGUgQ2hhaXJzgSEAGb9ECWmEzf6FQbrB\ 119 | Z9w7lshQhqowtrbLDFw4rXAxZuE= 120 | """.decoded 121 | #expect(throws: Never.self) { 122 | try Curve25519.Signing.PrivateKey(importing: key, format: .pkcs8) 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Cryptography/JWKSetTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWKSetTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 12/30/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct JWKSetTests { 14 | let jwksData: Data = .init(""" 15 | {"keys": 16 | [ 17 | {"kty":"EC", 18 | "crv":"P-256", 19 | "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 20 | "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 21 | "use":"enc", 22 | "kid":"1"}, 23 | {"kty":"RSA", 24 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx\ 25 | 4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs\ 26 | tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2\ 27 | QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI\ 28 | SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb\ 29 | w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 30 | "e":"AQAB", 31 | "alg":"RS256", 32 | "kid":"2011-04-29"}, 33 | {"kty":"EC", 34 | "crv":"P-256", 35 | "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 36 | "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 37 | "d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", 38 | "use":"enc", 39 | "kid":"1"}, 40 | {"kty":"RSA", 41 | "iat": 123972394872, 42 | "exp": 123974394972, 43 | "revoked": { 44 | "revoked_at": 123972495172, 45 | "reason": "compromised", 46 | }, 47 | "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4\ 48 | cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMst\ 49 | n64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2Q\ 50 | vzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbIS\ 51 | D08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw\ 52 | 0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 53 | "e":"AQAB", 54 | "d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9\ 55 | M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqij\ 56 | wp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d\ 57 | _cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBz\ 58 | nbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFz\ 59 | me1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", 60 | "p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPV\ 61 | nwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqV\ 62 | WlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", 63 | "q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyum\ 64 | qjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgx\ 65 | kIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", 66 | "dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oim\ 67 | YwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_Nmtu\ 68 | YZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", 69 | "dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUU\ 70 | vMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9\ 71 | GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", 72 | "qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzg\ 73 | UIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rx\ 74 | yR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", 75 | "alg":"RS256", 76 | "kid":"2011-04-29"} 77 | ] 78 | } 79 | """.utf8) 80 | 81 | let jwksPublicData: Data = .init(""" 82 | {"keys": 83 | [ 84 | {"kty":"RSA", 85 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx\ 86 | 4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMs\ 87 | tn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2\ 88 | QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbI\ 89 | SD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqb\ 90 | w0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 91 | "e":"AQAB", 92 | "alg":"RS256", 93 | "kid":"2011-04-29"}, 94 | {"kty":"EC", 95 | "crv":"P-256", 96 | "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", 97 | "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", 98 | "use":"enc", 99 | "kid":"1"} 100 | ] 101 | } 102 | """.utf8) 103 | 104 | @Test 105 | func decode() throws { 106 | let jwks = try JSONDecoder().decode(JSONWebKeySet.self, from: jwksData) 107 | try #require(jwks.count == 2) 108 | #expect(jwks.publicKeyset[0] is (any JSONWebValidatingKey)) 109 | #expect(jwks.publicKeyset[0] is JSONWebECPublicKey) 110 | 111 | #expect(jwks.publicKeyset[1] is (any JSONWebValidatingKey)) 112 | #expect(jwks.publicKeyset[1] is JSONWebRSAPublicKey) 113 | 114 | #expect(jwks[0] is (any JSONWebValidatingKey)) 115 | #expect(jwks[0] is (any JSONWebSigningKey)) 116 | #expect(jwks[0] is JSONWebECPrivateKey) 117 | #expect(!(jwks[0] is JSONWebECPublicKey)) 118 | 119 | #expect(jwks[1] is (any JSONWebValidatingKey)) 120 | #expect(jwks[1] is (any JSONWebSigningKey)) 121 | #expect(jwks[1] is JSONWebRSAPrivateKey) 122 | #expect(!(jwks[1] is JSONWebRSAPublicKey)) 123 | #expect(jwks[1].issuedAt == .init(timeIntervalSince1970: 123_972_394_872)) 124 | #expect(jwks[1].revoked == JSONWebKeyRevocation(at: .init(timeIntervalSince1970: 123_972_495_172), for: .compromised)) 125 | } 126 | 127 | @Test 128 | func encode() throws { 129 | let jwks = try JSONDecoder().decode(JSONWebKeySet.self, from: jwksData) 130 | _ = try JSONEncoder().encode(jwks) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Cryptography/ThumbprintTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThumbprintTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 4/18/24. 6 | // 7 | 8 | import Crypto 9 | import Foundation 10 | import Testing 11 | @testable import JWSETKit 12 | #if canImport(CommonCrypto) 13 | import CommonCrypto 14 | #endif 15 | 16 | @Suite 17 | struct ThumbprintTests { 18 | let keyData: Data = .init(""" 19 | { 20 | "kty": "RSA", 21 | "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt\ 22 | VT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn6\ 23 | 4tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FD\ 24 | W2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n9\ 25 | 1CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINH\ 26 | aQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", 27 | "e": "AQAB", 28 | "alg": "RS256", 29 | "kid": "2011-04-29" 30 | } 31 | """.utf8) 32 | 33 | @Test 34 | func jwkThumbprint() throws { 35 | let key = try JSONWebRSAPublicKey(importing: keyData, format: .jwk) 36 | let thumbprint = try key.thumbprint(format: .jwk, using: SHA256.self) 37 | #expect(thumbprint.data == Data(urlBase64Encoded: "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs")) 38 | } 39 | 40 | @Test 41 | func jwkAmbiguousThumbprint() throws { 42 | var key = try JSONWebRSAPublicKey(importing: keyData, format: .jwk) 43 | key.exponent = Data([0x00, 0x01, 0x00, 0x01]) 44 | let thumbprint = try key.thumbprint(format: .jwk, using: SHA256.self) 45 | #expect(thumbprint.data == Data(urlBase64Encoded: "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs")) 46 | } 47 | 48 | @Test 49 | func spkiThumbprint() throws { 50 | let key = try JSONWebRSAPublicKey(importing: keyData, format: .jwk) 51 | let thumbprint = try key.thumbprint(format: .spki, using: SHA256.self) 52 | #expect(thumbprint.data == Data(urlBase64Encoded: "rTIyDPbFltiEsFOBulc6uo3dV0m03o9KI6efmondrrI")) 53 | } 54 | 55 | @Test 56 | func jwk_URI_Thumbprint() throws { 57 | let key = try JSONWebRSAPublicKey(importing: keyData, format: .jwk) 58 | let uri = try key.thumbprintUri(format: .jwk, using: SHA256.self) 59 | #expect(uri == "urn:ietf:params:oauth:jwk-thumbprint:sha-256:NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs") 60 | } 61 | 62 | #if canImport(CommonCrypto) 63 | @Test 64 | func ecdsaThumbprint() throws { 65 | let secECKey = try SecKey(algorithm: .ecdsaSignatureP256SHA256) 66 | let ecKey = try P256.Signing.PrivateKey(derRepresentation: secECKey.exportKey(format: .pkcs8)) 67 | let pkcs8 = try secECKey.exportKey(format: .pkcs8) 68 | #expect(ecKey.derRepresentation == pkcs8) 69 | } 70 | #endif 71 | } 72 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Entities/JOSEHeaderJWETests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JOSEHeaderJWETests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct JOSEHeaderJWETests { 14 | let testClaims = """ 15 | { 16 | "enc":"A128GCM", 17 | "zip":"DEF" 18 | } 19 | """ 20 | @Test 21 | func encodeParams() throws { 22 | let decoder = JSONDecoder() 23 | let claims = try decoder.decode(JOSEHeader.self, from: .init(testClaims.utf8)) 24 | 25 | #expect(claims.encryptionAlgorithm == .aesEncryptionGCM128) 26 | #expect(claims.compressionAlgorithm == .deflate) 27 | } 28 | 29 | @Test 30 | func decodeParams() throws { 31 | var claims = JOSEHeader(storage: .init()) 32 | 33 | claims.encryptionAlgorithm = .aesEncryptionGCM128 34 | claims.compressionAlgorithm = .deflate 35 | let decoder = JSONDecoder() 36 | let decodedClaims = try decoder.decode(JOSEHeader.self, from: .init(testClaims.utf8)) 37 | 38 | #expect(claims == decodedClaims) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Entities/JWTOAuthClaimsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTOAuthClaimsTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct JWTOAuthClaimsTests { 14 | let testClaims = """ 15 | { 16 | "client_id": "s6BhdRkqt3", 17 | "scope": "openid profile reademail" 18 | } 19 | """ 20 | 21 | @Test 22 | func encodeParams() throws { 23 | let decoder = JSONDecoder() 24 | let claims = try decoder.decode(JSONWebTokenClaims.self, from: .init(testClaims.utf8)) 25 | 26 | #expect(claims.clientID == "s6BhdRkqt3") 27 | 28 | #expect(claims.scope == "openid profile reademail") 29 | #expect(claims.scopes == ["openid", "profile", "reademail"]) 30 | } 31 | 32 | @Test 33 | func decodeParams() throws { 34 | var claims = JSONWebTokenClaims(storage: .init()) 35 | claims.clientID = "s6BhdRkqt3" 36 | claims.scopes = ["openid", "profile", "reademail"] 37 | 38 | let decoder = JSONDecoder() 39 | let decodedClaims = try decoder.decode(JSONWebTokenClaims.self, from: .init(testClaims.utf8)) 40 | 41 | #expect(claims == decodedClaims) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Entities/JWTOIDCAuthClaimsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTOIDCAuthClaimsTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct JWTOIDCAuthClaimsTests { 14 | let testClaims = """ 15 | { 16 | "azp": "s6BhdRkqt3", 17 | "nonce": "n-0S6_WzA2Mj", 18 | "auth_time": 1311280969, 19 | "acr": "urn:mace:incommon:iap:silver", 20 | "amr": ["password", "otp"], 21 | "at_hash": "77QmUPtjPfzWtF2AnpK9RQ", 22 | "c_hash": "LDktKdoQak3Pk0cnXxCltA" 23 | } 24 | """ 25 | 26 | @Test 27 | func encodeParams() throws { 28 | let decoder = JSONDecoder() 29 | let claims = try decoder.decode(JSONWebTokenClaims.self, from: .init(testClaims.utf8)) 30 | 31 | #expect(claims.authorizedParty == "s6BhdRkqt3") 32 | #expect(claims.nonce == "n-0S6_WzA2Mj") 33 | #expect(claims.authTime == Date(timeIntervalSince1970: 1_311_280_969)) 34 | #expect(claims.authenticationContextClassReference == "urn:mace:incommon:iap:silver") 35 | #expect(claims.authenticationMethodsReferences == ["password", "otp"]) 36 | #expect(claims.accessTokenHash == Data(urlBase64Encoded: "77QmUPtjPfzWtF2AnpK9RQ")) 37 | #expect(claims.codeHash == Data(urlBase64Encoded: "LDktKdoQak3Pk0cnXxCltA")) 38 | } 39 | 40 | @Test 41 | func decodeParams() throws { 42 | var claims = JSONWebTokenClaims(storage: .init()) 43 | claims.authorizedParty = "s6BhdRkqt3" 44 | claims.nonce = "n-0S6_WzA2Mj" 45 | claims.authTime = Date(timeIntervalSince1970: 1_311_280_969) 46 | claims.authenticationContextClassReference = "urn:mace:incommon:iap:silver" 47 | claims.authenticationMethodsReferences = ["password", "otp"] 48 | claims.accessTokenHash = Data(urlBase64Encoded: "77QmUPtjPfzWtF2AnpK9RQ") 49 | claims.codeHash = Data(urlBase64Encoded: "LDktKdoQak3Pk0cnXxCltA") 50 | 51 | let decoder = JSONDecoder() 52 | let decodedClaims = try decoder.decode(JSONWebTokenClaims.self, from: .init(testClaims.utf8)) 53 | 54 | #expect(claims == decodedClaims) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Entities/JWTOIDCStandardClaimsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTOIDCStandardClaimsTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct JWTOIDCStandardClaimsTests { 14 | let testClaims = """ 15 | { 16 | "name": "Jane Doe", 17 | "given_name": "Jane", 18 | "middle_name": "F.", 19 | "family_name": "Doe", 20 | "nickname": "Jane", 21 | "preferred_username": "j.doe", 22 | "profile": "http://example.com/janedoe/profile", 23 | "picture": "http://example.com/janedoe/me.jpg", 24 | "website": "http://example.com/janedoe", 25 | "email": "janedoe@example.com", 26 | "email_verified": true, 27 | "gender": "female", 28 | "birthdate": "2000-10-31", 29 | "zoneinfo": "Asia/Tehran", 30 | "locale": "en-US", 31 | "phone_number": "+1 (310) 123-4567", 32 | "phone_number_verified": true, 33 | "address": { 34 | "street_address": "1234 Hollywood Blvd.", 35 | "locality": "Los Angeles", 36 | "region": "CA", 37 | "postal_code": "90210", 38 | "country": "US"}, 39 | "updated_at": 1311280970 40 | } 41 | """ 42 | 43 | @Test 44 | func encodeParams() throws { 45 | let decoder = JSONDecoder() 46 | let claims = try decoder.decode(JSONWebTokenClaims.self, from: .init(testClaims.utf8)) 47 | 48 | let formatter = ISO8601DateFormatter() 49 | formatter.formatOptions = .withFullDate 50 | 51 | #expect(claims.name == "Jane Doe") 52 | #expect(claims.givenName == "Jane") 53 | #expect(claims.middleName == "F.") 54 | #expect(claims.familyName == "Doe") 55 | #expect(claims.nickname == "Jane") 56 | #expect(claims.preferredUsername == "j.doe") 57 | #expect(claims.profileURL == URL(string: "http://example.com/janedoe/profile")) 58 | #expect(claims.pictureURL == URL(string: "http://example.com/janedoe/me.jpg")) 59 | #expect(claims.websiteURL == URL(string: "http://example.com/janedoe")) 60 | #expect(claims.email == "janedoe@example.com") 61 | #expect(claims.isEmailVerified == true) 62 | #expect(claims.gender == "female") 63 | #expect(claims.birthdate == formatter.date(from: "2000-10-31")) 64 | #expect(claims.zoneInfo == TimeZone(abbreviation: "IRST")) 65 | #expect(claims.locale == Locale(identifier: "en_US")) 66 | #expect(claims.phoneNumber == "+1 (310) 123-4567") 67 | #expect(claims.isPhoneNumberVerified == true) 68 | #expect(claims.address == JSONWebAddress(streetAddress: "1234 Hollywood Blvd.", locality: "Los Angeles", region: "CA", postalCode: "90210", country: "US")) 69 | #expect(claims.updatedAt == Date(timeIntervalSince1970: 1_311_280_970)) 70 | } 71 | 72 | @Test 73 | func decodeParams() throws { 74 | let formatter = ISO8601DateFormatter() 75 | formatter.formatOptions = .withFullDate 76 | 77 | var claims = JSONWebTokenClaims(storage: .init()) 78 | claims.name = "Jane Doe" 79 | claims.givenName = "Jane" 80 | claims.middleName = "F." 81 | claims.familyName = "Doe" 82 | claims.nickname = "Jane" 83 | claims.preferredUsername = "j.doe" 84 | claims.profileURL = URL(string: "http://example.com/janedoe/profile") 85 | claims.pictureURL = URL(string: "http://example.com/janedoe/me.jpg") 86 | claims.websiteURL = URL(string: "http://example.com/janedoe") 87 | claims.email = "janedoe@example.com" 88 | claims.isEmailVerified = true 89 | claims.gender = "female" 90 | claims.birthdate = .init(timeIntervalSince1970: 972_950_400) 91 | claims.zoneInfo = TimeZone(abbreviation: "IRST") 92 | claims.locale = Locale(identifier: "en-US") 93 | claims.phoneNumber = "+1 (310) 123-4567" 94 | claims.isPhoneNumberVerified = true 95 | claims.address = JSONWebAddress( 96 | streetAddress: "1234 Hollywood Blvd.", locality: "Los Angeles", 97 | region: "CA", postalCode: "90210", country: "US" 98 | ) 99 | claims.updatedAt = Date(timeIntervalSince1970: 1_311_280_970) 100 | 101 | let decoder = JSONDecoder() 102 | let decodedClaims = try decoder.decode(JSONWebTokenClaims.self, from: .init(testClaims.utf8)) 103 | 104 | #expect(claims == decodedClaims) 105 | } 106 | 107 | @Test 108 | func localized() throws { 109 | var claims = JSONWebTokenClaims(storage: .init()) 110 | let enLocale = Locale(identifier: "en") 111 | let faLocale = Locale(identifier: "fa") 112 | let faIRLocale = Locale(identifier: "fa_IR") 113 | claims.name = "Jane Doe" 114 | claims[\.name, faLocale] = "جین دو" 115 | 116 | #expect(claims.name == "Jane Doe") 117 | #expect(claims[\.name, faIRLocale] == "جین دو") 118 | #expect(claims[\.name, faLocale] == "جین دو") 119 | #expect(claims[\.name, enLocale] == "Jane Doe") 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Entities/JWTRegisteredClaimsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTRegisteredClaimsTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/17/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | #if canImport(Darwin) 13 | extension JSONWebContainerCustomParameters { 14 | var iat: Date? { fatalError() } 15 | } 16 | #endif 17 | 18 | @Suite 19 | struct JWTRegisteredClaimsTests { 20 | let testClaims = """ 21 | { 22 | "iss": "https://self-issued.me", 23 | "sub": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs", 24 | "aud": "https://client.example.org/cb", 25 | "exp": 1311281970, 26 | "iat": 1311280970, 27 | "nbf": 1311280970, 28 | "jti": "88150e93-6dc8-4a7a-bb47-8b6052d62875" 29 | } 30 | """ 31 | 32 | @Test 33 | func decodeClaims() throws { 34 | let decoder = JSONDecoder() 35 | let claims = try decoder.decode(JSONWebTokenClaims.self, from: .init(testClaims.utf8)) 36 | 37 | #expect(claims.issuer == "https://self-issued.me") 38 | #expect(claims.issuerURL == URL(string: "https://self-issued.me")) 39 | 40 | #expect(claims.subject == "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs") 41 | #expect(claims.subjectURL?.host == nil) 42 | 43 | #expect(claims.audience == ["https://client.example.org/cb"]) 44 | #expect(claims.audienceURL == [URL(string: "https://client.example.org/cb")!]) 45 | 46 | #expect(claims.expiry == Date(timeIntervalSince1970: 1_311_281_970)) 47 | #expect(claims.exp == Date(timeIntervalSince1970: 1_311_281_970)) 48 | #expect(claims["exp"] == 1_311_281_970) 49 | 50 | #expect(claims.issuedAt == Date(timeIntervalSince1970: 1_311_280_970)) 51 | #expect(claims.iat == Date(timeIntervalSince1970: 1_311_280_970)) 52 | #expect(claims["iat"] == 1_311_280_970) 53 | 54 | #expect(claims.notBefore == Date(timeIntervalSince1970: 1_311_280_970)) 55 | #expect(claims["nbf"] == Date(timeIntervalSince1970: 1_311_280_970)) 56 | #expect(claims.nbf == 1_311_280_970) 57 | 58 | #expect(claims.jwtId == "88150e93-6dc8-4a7a-bb47-8b6052d62875") 59 | #expect(claims.jwtUUID == UUID(uuidString: "88150E93-6DC8-4A7A-BB47-8B6052D62875")) 60 | } 61 | 62 | @Test 63 | func encodeClaims() throws { 64 | var claims = JSONWebTokenClaims(storage: .init()) 65 | claims.issuerURL = URL(string: "https://self-issued.me") 66 | claims.subject = "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" 67 | claims.audienceURL = [URL(string: "https://client.example.org/cb")!] 68 | claims.expiry = Date(timeIntervalSince1970: 1_311_281_970) 69 | claims.issuedAt = Date(timeIntervalSince1970: 1_311_280_970) 70 | claims.notBefore = Date(timeIntervalSince1970: 1_311_280_970) 71 | claims.jwtUUID = UUID(uuidString: "88150E93-6DC8-4A7A-BB47-8B6052D62875") 72 | 73 | let decoder = JSONDecoder() 74 | let decodedClaims = try decoder.decode(JSONWebTokenClaims.self, from: .init(testClaims.utf8)) 75 | 76 | #expect(claims["exp"] == 1_311_281_970) 77 | #expect(claims == decodedClaims) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Entities/JWTTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JWTTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/21/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct JWTTests { 14 | let jwtString = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYXVkIjoiZ29vZ2xlLmNvbSIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMiwibmJmIjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDkwMjJ9.vGoQSvaLlU1lh_rsJT-vCPG6DNe_a9rHeJiezXRswKQ" 15 | 16 | @Test 17 | func decode() throws { 18 | let jwt = try JSONWebToken(from: jwtString) 19 | #expect(jwt.signatures.first?.protected.algorithm == .hmacSHA256) 20 | #expect(jwt.payload.value.issuedAt == Date(timeIntervalSince1970: 1_516_239_022)) 21 | #expect(throws: Never.self) { try jwt.verifySignature(using: ExampleKeys.symmetric) } 22 | } 23 | 24 | @Test 25 | func encode() throws { 26 | let jwt = try JSONWebToken(from: jwtString) 27 | #expect(try String(jwt) == jwtString) 28 | #expect(jwt.description == jwtString) 29 | } 30 | 31 | @Test 32 | func testInit() throws { 33 | let payload = try JSONWebTokenClaims { container in 34 | container.issuedAt = .init() 35 | container.expiry = .init(timeIntervalSinceNow: 3600) 36 | container.jwtUUID = .init() 37 | } 38 | let jwt = try JSONWebToken(payload: payload, algorithm: .hmacSHA256, using: ExampleKeys.symmetric) 39 | #expect(throws: Never.self) { try jwt.verifySignature(using: JSONWebKeySet(keys: [ExampleKeys.symmetric])) } 40 | } 41 | 42 | @Test 43 | func verify() throws { 44 | let jwt = try JSONWebToken(from: jwtString) 45 | #expect(throws: Never.self) { try jwt.verifyDate(.init(timeIntervalSince1970: 1_516_239_024)) } 46 | #expect(throws: JSONWebValidationError.self) { try jwt.verifyDate(.init(timeIntervalSince1970: 1_516_239_021)) } 47 | #expect(throws: JSONWebValidationError.self) { try jwt.verifyDate(.init(timeIntervalSince1970: 1_516_249_024)) } 48 | #expect(throws: Never.self) { try jwt.verifyAudience(includes: "google.com") } 49 | #expect(throws: JSONWebValidationError.self) { try jwt.verifyAudience(includes: "yahoo.com") } 50 | } 51 | 52 | #if canImport(Foundation.NSURLSession) 53 | @Test 54 | func authorization() throws { 55 | let jwt = try JSONWebToken(from: jwtString) 56 | var request = URLRequest(url: .init(string: "https://www.example.com/")!) 57 | request.authorizationToken = jwt 58 | #expect(request.authorizationToken == jwt) 59 | #expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer \(jwtString)") 60 | } 61 | #endif 62 | } 63 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Extensions/Base64Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Base64Tests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/16/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | struct Base64Tests { 13 | @Test 14 | func decode() throws { 15 | let encoded = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" 16 | let value = "{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}" 17 | #expect(Data(urlBase64Encoded: encoded) == Data(value.utf8)) 18 | 19 | let encoded2 = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" 20 | let value2 = "{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}" 21 | #expect(Data(urlBase64Encoded: encoded2) == Data(value2.utf8)) 22 | } 23 | 24 | @Test 25 | func encode() throws { 26 | let encoded = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" 27 | let value = "{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}" 28 | #expect(Data(value.utf8).urlBase64EncodedData() == Data(encoded.utf8)) 29 | 30 | let encoded2 = "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" 31 | let value2 = "{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n \"http://example.com/is_root\":true}" 32 | #expect(Data(value2.utf8).urlBase64EncodedData() == Data(encoded2.utf8)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Extensions/KeyLookupTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyLookupTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/16/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct KeyLookupTests { 14 | @Test 15 | func jsonWebKeyNormalizer() throws { 16 | #expect("camelCase".jsonWebKey == "camel_case") 17 | #expect("clientID".jsonWebKey == "client_id") 18 | #expect("authTime".jsonWebKey == "auth_time") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/JWSETKitTests/Extensions/LocalizingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizingTests.swift 3 | // 4 | // 5 | // Created by Amir Abbas Mousavian on 9/16/23. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable import JWSETKit 11 | 12 | @Suite 13 | struct LocalizingTests { 14 | @Test 15 | func errorLocalizing() throws { 16 | #if canImport(Darwin) 17 | let date = Date(timeIntervalSince1970: 0) 18 | 19 | let enLocale = Locale(identifier: "en-US") 20 | #expect(JSONWebKeyError.unknownAlgorithm.localizedError(for: enLocale) == "Given signature/encryption algorithm is no supported.") 21 | #expect(JSONWebValidationError.tokenExpired(expiry: date).localizedError(for: enLocale).hasPrefix("Token is invalid after ")) 22 | 23 | let faLocale = Locale(identifier: "fa-IR") 24 | #expect(JSONWebKeyError.unknownAlgorithm.localizedError(for: faLocale) == "الگوریتم انتخابی برای امضا/رمز پشتیبانی نمی‌شود.") 25 | #expect(JSONWebValidationError.tokenExpired(expiry: date).localizedError(for: faLocale).hasPrefix("توکن برای پس از")) 26 | 27 | #expect(JSONWebKeyError.unknownAlgorithm.errorDescription != nil) 28 | #endif 29 | } 30 | 31 | @Test 32 | func testBestMatch() throws { 33 | #expect(Locale(bcp47: "fa-IR").identifier == "fa_IR") 34 | 35 | #expect(Locale(identifier: "fa-IR").bestMatch(in: [ 36 | .init(identifier: "en-US"), 37 | .init(identifier: "en-IR"), 38 | .init(identifier: "fa-AF"), 39 | .init(identifier: "fa"), 40 | ])?.identifier == "fa") 41 | } 42 | } 43 | --------------------------------------------------------------------------------