├── .DS_Store ├── .envrc ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── rust.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Changelog.md ├── LICENSE.md ├── SECURITY.md ├── assets ├── RustyPasetoBatteriesIncludedArchitecture.png ├── RustyPasetoCoreArchitecture.png ├── RustyPasetoGenericArchitecture.png ├── RustyPasetoLogo.png ├── RustyPasetoPreludeArchitecture.png ├── RustyPasetoV4LocalArchitecture.png └── paseto_logo.png ├── examples └── actix_identity │ ├── main.rs │ ├── paseto.rs │ └── readme.md ├── flake.lock ├── flake.nix ├── funding.yml ├── readme.md ├── readme_crates_io.md ├── rustfmt.toml ├── src ├── core │ ├── common │ │ ├── authentication_key.rs │ │ ├── authentication_key_impl │ │ │ ├── mod.rs │ │ │ ├── v1_local.rs │ │ │ ├── v3_local.rs │ │ │ └── v4_local.rs │ │ ├── authentication_key_separator.rs │ │ ├── cipher_text.rs │ │ ├── cipher_text_impl │ │ │ ├── mod.rs │ │ │ ├── v1_local.rs │ │ │ ├── v1_public.rs │ │ │ ├── v2_local.rs │ │ │ ├── v3_local.rs │ │ │ └── v4_local.rs │ │ ├── encryption_key.rs │ │ ├── encryption_key_impl │ │ │ ├── mod.rs │ │ │ ├── v1_local.rs │ │ │ ├── v3_local.rs │ │ │ ├── v4_local.rs │ │ │ └── v_local.rs │ │ ├── encryption_key_separator.rs │ │ ├── encryption_nonce.rs │ │ ├── hkdf_key.rs │ │ ├── mod.rs │ │ ├── pre_authentication_encoding.rs │ │ ├── raw_payload.rs │ │ ├── raw_payload_impl │ │ │ ├── mod.rs │ │ │ ├── nist_local.rs │ │ │ ├── v2_local.rs │ │ │ ├── v4_local.rs │ │ │ └── v_public.rs │ │ ├── tag.rs │ │ └── tag_impl │ │ │ ├── mod.rs │ │ │ ├── nist_local.rs │ │ │ └── v4_local.rs │ ├── error.rs │ ├── footer.rs │ ├── header.rs │ ├── implicit_assertion.rs │ ├── key │ │ ├── keys.rs │ │ ├── mod.rs │ │ ├── paseto_asymmetric_private_key.rs │ │ ├── paseto_asymmetric_public_key.rs │ │ ├── paseto_nonce.rs │ │ ├── paseto_nonce_impl │ │ │ ├── mod.rs │ │ │ ├── v1_local.rs │ │ │ ├── v2_local.rs │ │ │ ├── v2_public.rs │ │ │ ├── v3_local.rs │ │ │ └── v4_local.rs │ │ └── paseto_symmetric_key.rs │ ├── mod.rs │ ├── paseto.rs │ ├── paseto_impl │ │ ├── mod.rs │ │ ├── v1_local.rs │ │ ├── v1_public.rs │ │ ├── v2_local.rs │ │ ├── v2_public.rs │ │ ├── v3_local.rs │ │ ├── v3_public.rs │ │ ├── v4_local.rs │ │ └── v4_public.rs │ ├── payload.rs │ ├── purpose │ │ ├── local.rs │ │ ├── mod.rs │ │ └── public.rs │ ├── traits.rs │ └── version │ │ ├── mod.rs │ │ ├── v1.rs │ │ ├── v2.rs │ │ ├── v3.rs │ │ └── v4.rs ├── generic │ ├── builders │ │ ├── error.rs │ │ ├── generic_builder.rs │ │ ├── mod.rs │ │ └── traits.rs │ ├── claims │ │ ├── audience_claim.rs │ │ ├── custom_claim.rs │ │ ├── error.rs │ │ ├── expiration_claim.rs │ │ ├── issued_at_claim.rs │ │ ├── issuer_claim.rs │ │ ├── mod.rs │ │ ├── not_before_claim.rs │ │ ├── subject_claim.rs │ │ ├── token_identifier_claim.rs │ │ └── traits.rs │ ├── mod.rs │ └── parsers │ │ ├── error.rs │ │ ├── generic_parser.rs │ │ └── mod.rs ├── lib.rs └── prelude │ ├── error.rs │ ├── mod.rs │ ├── paseto_builder.rs │ └── paseto_parser.rs └── tests ├── build_payload_from_claims_prop_test.proptest-regressions ├── build_payload_from_claims_prop_test.rs ├── generic_claims_wrap_value_prop_test.proptest-regressions ├── generic_claims_wrap_value_prop_test.rs ├── v1_public_test_vectors_private_key.der ├── v1_public_test_vectors_private_key.pk8 ├── v1_public_test_vectors_public_key.der ├── version1_test_vectors.rs ├── version2_test_vectors.rs ├── version3_test_vectors.rs └── version4_test_vectors.rs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/.DS_Store -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.* text eol=lf 2 | 3 | *.png binary 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **IMPORTANT: Please do not create a Pull Request without creating an issue first.** 2 | 3 | *Any change needs to be discussed before proceeding. Failure to do so may result in the rejection of the pull request.* 4 | 5 | Please provide enough information so that others can review your pull request: 6 | 7 | 8 | 9 | Explain the **details** for making this change. What existing problem does the pull request solve? 10 | 11 | 12 | 13 | **Test plan (required)** 14 | 15 | Demonstrate the code is solid. Example: The exact commands you ran and their output, screenshots / videos if the pull request changes UI. 16 | 17 | 18 | 19 | **Code formatting** 20 | 21 | 22 | 23 | **Closing issues** 24 | 25 | Put `closes #XXXX` in your comment to auto-close the issue that your PR fixes (if such). 26 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Continuous integration 3 | jobs: 4 | check: 5 | name: Check 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - uses: actions-rs/toolchain@v1 10 | with: 11 | profile: minimal 12 | toolchain: stable 13 | override: true 14 | - uses: actions-rs/cargo@v1 15 | with: 16 | command: check 17 | test: 18 | name: Test Suite 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | feature: 23 | - v1_local 24 | - v1_public 25 | - v2_local 26 | - v2_public 27 | - v3_local 28 | - v3_public 29 | - v4_local 30 | - v4_public 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: actions-rs/toolchain@v1 34 | with: 35 | profile: minimal 36 | toolchain: stable 37 | override: true 38 | - uses: actions-rs/cargo@v1 39 | with: 40 | command: test 41 | args: --no-default-features --features ${{ matrix.feature }} 42 | audit: 43 | name: Security Audit 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v3 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | profile: minimal 50 | toolchain: stable 51 | override: true 52 | - run: cargo install cargo-audit 53 | - uses: actions-rs/audit-check@v1.2.0 54 | with: 55 | token: ${{ secrets.GITHUB_TOKEN }} 56 | - run: cargo audit 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /target 3 | Cargo.lock 4 | **/.envrc 5 | /.direnv 6 | tests/generated_tests.rs 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | rolandrodriguez@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rusty_paseto" 3 | version = "0.7.2" 4 | edition = "2021" 5 | readme = "readme.md" 6 | authors = ["Roland Rodriguez "] 7 | description = "A type-driven, ergonomic alternative to JWT for secure stateless PASETO tokens." 8 | repository = "https://github.com/rrrodzilla/rusty_paseto" 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["paseto", "token", "security", "api", "web"] 11 | categories = [ 12 | "cryptography", 13 | "authentication", 14 | "encoding", 15 | "network-programming", 16 | "web-programming", 17 | ] 18 | documentation = "https://docs.rs/rusty_paseto/latest/rusty_paseto/" 19 | 20 | [features] 21 | v1 = [] 22 | v2 = [] 23 | v3 = [] 24 | v4 = [] 25 | public = [] 26 | local = [] 27 | v1_local = ["v1", "local", "core", "aes", "chacha20", "hmac", "sha2", "blake2"] 28 | v2_local = ["v2", "local", "core", "blake2", "chacha20poly1305"] 29 | v3_local = ["v3", "local", "core", "aes", "hmac", "sha2", "chacha20"] 30 | v4_local = ["v4", "local", "core", "blake2", "chacha20"] 31 | v1_public = ["v1", "public", "core", "ed25519-dalek"] 32 | v2_public = ["v2", "public", "core", "ed25519-dalek", "ring/std"] 33 | v3_public = ["v3", "public", "core", "p384", "sha2"] 34 | v4_public = ["v4", "public", "core", "ed25519-dalek", "ring/std"] 35 | core = [] 36 | generic = ["core", "serde", "erased-serde", "serde_json"] 37 | batteries_included = ["generic"] 38 | default = ["batteries_included", "v4_local", "v4_public"] 39 | 40 | [lib] 41 | doctest = true 42 | 43 | 44 | [dependencies] 45 | p384 = { version = "0.13", optional = true } 46 | chacha20 = { version = "0.9", optional = true } 47 | blake2 = { version = "0.10", optional = true } 48 | chacha20poly1305 = { version = "0.10", optional = true } 49 | ring = { version = "0.17", features = ["std"], optional = false } 50 | base64 = { version = "0.22", optional = false } 51 | hex = { version = "0.4", optional = false } 52 | serde = { version = "1.0", features = ["derive"], optional = true } 53 | ed25519-dalek = { version = "2.0", features = ["zeroize"], optional = true } 54 | serde_json = { version = "1.0", optional = true } 55 | thiserror = "1.0" 56 | iso8601 = "0.6" 57 | erased-serde = { version = "0.4", optional = true } 58 | aes = { version = "0.7", features = ["ctr"], optional = true } 59 | hmac = { version = "0.12", optional = true } 60 | sha2 = { version = "0.10", optional = true } 61 | zeroize = { version = "1.4", features = ["zeroize_derive"] } 62 | time = { version = "0.3", features = ["parsing", "formatting"] } 63 | rand_core = "0.6" 64 | digest = "0.10" 65 | 66 | [dev-dependencies] 67 | anyhow = "1.0" 68 | serde_json = { version = "1.0" } 69 | primes = "0.3" 70 | actix-web = "4" 71 | actix-identity = "0.4" 72 | tokio = "1.17" 73 | actix-utils = "3.0" 74 | uuid = { version = "1.8", features = ["v4"] } 75 | proptest = "1.4" 76 | erased-serde = { version = "0.4" } 77 | 78 | [[example]] 79 | name = "actix_identity" 80 | required-features = ["default"] 81 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # v0.1.11 (2021-10-21) 2 | 3 | - Create initial commit 4 | - Rename structs, move mods, refactor traits 5 | - Generalize dependencies with trait bounds 6 | - Rename unit test mods, add v2localheader struct 7 | - Refactor entire project 8 | - Add all 9 shared key test vector cases 9 | - Rename some structs and complete minor edits 10 | - Add strongly typed claims 11 | - Refactor arbitrary claim to use try_from trait 12 | - Tighten up arbitrary claim api 13 | - Rename claim structs and fix lifetime issues 14 | - Refactor most structs to generics 15 | - Update readme 16 | - Update minor version in Cargo.toml 17 | - Touch cargo.toml to test git editor 18 | - Repair the readme file from a poor merge 19 | 20 | ### Notes 21 | 22 | - Not quite encrypting correctly. Working on getting the first test vector to pass. 23 | 24 | - Message struct renamed to Payload, moved around mods and refactored 25 | conversion traits. 26 | 27 | - Generalizing some dependencies by using trait bounds in several methods and trait implementations 28 | 29 | - Unit test mods were renamed for consistency 30 | 31 | - Went nuts and did a big refactor, traits, eliminated a struct or two, some other thing. :-| all good, tests passing, code looks pretty good. 32 | 33 | - All tests pass after addition. 34 | 35 | - Renamed V2LocalDecryptedString to V2LocalDecryptedToken and a couple minor edits 36 | 37 | - Still need to json serialize them properly after this commit 38 | 39 | - Refactored arbitrary claim to use try_from trait instead of custom try_new for api consistency 40 | 41 | - Tightened up arbitrary claim api and removed unused comments, small refactors 42 | 43 | - Renamed claim structs for consistency and fixed lifetime issues with borrowed strings 44 | 45 | - Major refactor to change most structs to generics using version and purpose as arguments 46 | 47 | additional struct refactors to accept generic version and purpose types 48 | 49 | - Update the project status in the readme file 50 | 51 | The PasetoBuilder and PasetoParser were incorrectly indicating that they 52 | were complete. They have not been started as of yet. 53 | 54 | - version update 55 | 56 | - Wanted to make sure nvim was opening correctly so I can start tightening 57 | up commit messages 58 | 59 | - - feature: Basic encryption and decryption 60 | 61 | - feature: Generic token building and parsing 62 | 63 | - feature: Flexible claim validation sans custom validation functions 64 | 65 | - feature: All v2.local [PASETO](https://github.com/paseto-standard/test-vectors/blob/master/v2.json) test vectors implemented and 66 | successfully passing 67 | # v0.1.10..v0.1.13 (2021-10-22) 68 | 69 | - Repair the readme file from a poor merge 70 | - Add optional closure for custom validation 71 | - Merge pull request #5 from rrrodzilla/claim_validation_issue_1 72 | - Add chrono to Cargo and add paseto_builder (#10) 73 | 74 | ### Notes 75 | 76 | - - feature: Basic encryption and decryption 77 | 78 | - feature: Generic token building and parsing 79 | 80 | - feature: Flexible claim validation sans custom validation functions 81 | 82 | - feature: All v2.local [PASETO](https://github.com/paseto-standard/test-vectors/blob/master/v2.json) test vectors implemented and 83 | successfully passing 84 | 85 | - Add optional closure for custom validation 86 | 87 | ### Additions 88 | 89 | - Added an optional closure argument to the validate_claim method. 90 | To be used to allow the user to provide custom validation logic for a 91 | particular claim 92 | 93 | - Added logic in the parse method to run custom validation closures 94 | if one is specified. This means claim validators will verify the 95 | claim exists and verify the value matches what is expected. If a 96 | custom closure is provided, the validator first checks the claim 97 | exists and then the value is provided to the closure for further 98 | validation by the end user. 99 | 100 | - PasetoTokenParseError::InvalidClaimValueType(String) for claim 101 | values we try to convert to an invalid type 102 | 103 | - PasetoTokenParseError::CustomClaimValidation for claims which 104 | fail in user provided custom validation closures 105 | 106 | - Implement Default trait on all reserved claims so that they can 107 | be passed into custom validation closures 108 | 109 | - Implement From(&str) for CustomClaim so that they can be passed 110 | into custom validation closures which always ignore passed in values 111 | when adding the claim to the validator 112 | 113 | - Move chrono from dev dependencies to dependencies 114 | 115 | - Added PasetoTokenBuilder in preparation for adding PASETO 116 | validation logic 117 | 118 | - extend_claims method to GenericTokenBuilder 119 | : bump patch version 0.1.13 120 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 rrrodzilla 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 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This crate has NOT been independently audited. As with any code you find on the web that has not been audited, usage of this crate is at your own risk. 4 | 5 | ## Reporting a Vulnerability 6 | 7 | Please report any issues to the maintainer by creating a bug report. Thank you for your contribution! 8 | -------------------------------------------------------------------------------- /assets/RustyPasetoBatteriesIncludedArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/assets/RustyPasetoBatteriesIncludedArchitecture.png -------------------------------------------------------------------------------- /assets/RustyPasetoCoreArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/assets/RustyPasetoCoreArchitecture.png -------------------------------------------------------------------------------- /assets/RustyPasetoGenericArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/assets/RustyPasetoGenericArchitecture.png -------------------------------------------------------------------------------- /assets/RustyPasetoLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/assets/RustyPasetoLogo.png -------------------------------------------------------------------------------- /assets/RustyPasetoPreludeArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/assets/RustyPasetoPreludeArchitecture.png -------------------------------------------------------------------------------- /assets/RustyPasetoV4LocalArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/assets/RustyPasetoV4LocalArchitecture.png -------------------------------------------------------------------------------- /assets/paseto_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/assets/paseto_logo.png -------------------------------------------------------------------------------- /examples/actix_identity/main.rs: -------------------------------------------------------------------------------- 1 | 2 | /// This example shows how you might use a PASETO token to store some data in a cookie once the 3 | /// user has been logged in. 4 | /// 5 | /// run the following command from your shell to: 6 | /// 1) visit the site as an anonymous user and then, 7 | /// 2) login the user, creating a paseto token and storing it in a cookie using the identity as the 8 | /// implicit assertion and then, 9 | /// 3) logout the user, forgetting the identity 10 | /// 4) attempting to visit a secure path as a logged out user (will panic) 11 | /// 12 | /// curl http://localhost:8080;curl -X POST http://localhost:8080/login -c ~/cookies; curl 13 | /// http://localhost:8080/app/secure -b ~/cookies; curl -X POST http://localhost:8080/logout -b 14 | /// ~/cookies 15 | /// 16 | use rusty_paseto::prelude::*; 17 | use actix_web::http::StatusCode; 18 | use actix_web::cookie::{Cookie, SameSite}; 19 | use actix_web::web; 20 | use actix_web::{post,get, HttpResponse, HttpServer, App, services}; 21 | use actix_identity::{Identity, CookieIdentityPolicy, IdentityService}; 22 | use time::OffsetDateTime; 23 | use uuid::Uuid; 24 | 25 | mod paseto; 26 | use paseto::PasetoCookieIdentityPolicy; 27 | 28 | #[get("/secure")] 29 | async fn secure(id: Identity) -> String { 30 | // access request identity 31 | if let Some(id) = id.identity() { 32 | format!("Logged in Secure User: {}\n", id) 33 | } else { 34 | "Welcome Anonymous!".to_owned() 35 | } 36 | } 37 | 38 | #[get("/")] 39 | async fn index(id: Identity) -> String { 40 | // access request identity 41 | if let Some(id) = id.identity() { 42 | format!("Welcome! {}", id) 43 | } else { 44 | println!("Found new anonymous user\n"); 45 | "Welcome Anonymous!\n".to_owned() 46 | } 47 | } 48 | 49 | #[post("/login")] 50 | async fn login(id: Identity, data: web::Data) -> HttpResponse { 51 | // here you might do whatever checks are needed to authenticate user 52 | 53 | // create a new identity and wrap it in an auth cookie 54 | let authenticated_user_id = Uuid::new_v4().to_string(); 55 | println!("Logged in user {}\n", authenticated_user_id); 56 | 57 | //create an implicit assertion (or you could create claim) 58 | let assertion = ImplicitAssertion::from(authenticated_user_id.as_str()); 59 | 60 | //creating a paseto key 61 | let key_val = data.paseto_key.as_bytes(); 62 | let key = PasetoSymmetricKey::::from(Key::from(key_val)); 63 | 64 | //create a token using the identity as an implicit assertion (you could also use a claim if 65 | //needed) 66 | let token = PasetoBuilder::::default() 67 | .set_implicit_assertion(assertion) 68 | .build(&key) 69 | .expect("Couldn't create paseto auth token"); 70 | 71 | // remember new authenticated identity 72 | id.remember(authenticated_user_id.to_string()); 73 | 74 | // return the response creating a new cookie to hold the token 75 | HttpResponse::build(StatusCode::OK) 76 | .cookie( 77 | Cookie::build("auth-token", token) 78 | .path("/") 79 | .expires(OffsetDateTime::now_utc()) 80 | // Using `secure(false)` so the example works over HTTP. 81 | // In production use `secure(true)` and handle errors gracefully. 82 | .secure(false) 83 | .http_only(true) 84 | .same_site(SameSite::Lax) 85 | .finish(), 86 | ) 87 | .finish() 88 | 89 | 90 | } 91 | 92 | #[post("/logout")] 93 | async fn logout(id: Identity) -> String { 94 | println!("Logging out user {}\n", id.identity().expect("Couldn't get identity")); 95 | //build logout msg 96 | let logout_msg = format!("Goodbye {}!\n", id.identity().expect("Couldn't get identity")).to_owned(); 97 | // remove identity 98 | id.forget(); 99 | logout_msg 100 | 101 | } 102 | 103 | //shared state 104 | pub(crate) struct AppData { 105 | paseto_key: &'static str 106 | } 107 | 108 | #[actix_web::main] // or #[tokio::main] 109 | async fn main() -> std::io::Result<()> { 110 | HttpServer::new(move || { 111 | // create cookie identity backend (inside closure, since policy is not Clone) 112 | let policy = IdentityService::new( 113 | CookieIdentityPolicy::new(&[0; 32]) 114 | .name("auth-cookie") 115 | // `secure(false)` is used for local development; set `secure(true)` when running over HTTPS. 116 | .secure(false), 117 | ); 118 | 119 | // create a paseto cookie policy, for this use case, however I recommend using a middleware but 120 | // this shows how you might use a policy instead 121 | let paseto_policy = PasetoCookieIdentityPolicy {}; 122 | 123 | //paths that are not verified with the paseto token 124 | let unauthenticated_scope = web::scope("").service(services![index, login, logout]); 125 | //paths that should verify that a token exists and is valid 126 | let authenticated_scope = web::scope("/app").wrap(IdentityService::new(paseto_policy)).service(services![secure]); 127 | 128 | //create and run the server 129 | App::new() 130 | .app_data(web::Data::new(AppData { paseto_key: "wubbalubbadubdubwubbalubbadubdub"})) 131 | .wrap(policy) 132 | // wrap policy into middleware identity middleware 133 | .service(authenticated_scope) 134 | .service(unauthenticated_scope) 135 | }) .bind(("127.0.0.1", 8080))? 136 | .run() 137 | .await 138 | } 139 | 140 | 141 | -------------------------------------------------------------------------------- /examples/actix_identity/paseto.rs: -------------------------------------------------------------------------------- 1 | use crate::AppData; 2 | use actix_identity::{IdentityPolicy, RequestIdentity}; 3 | use actix_utils::future::{ready, Ready}; 4 | use actix_web::{ 5 | dev::{ServiceRequest, ServiceResponse}, 6 | error::{Error, Result}, 7 | web::Data, 8 | }; 9 | use rusty_paseto::prelude::*; 10 | pub struct PasetoCookieIdentityPolicy {} 11 | 12 | fn validate_auth_token(request: &mut ServiceRequest) -> Result, Error> { 13 | //try to find the cookie with the auth token, panic if not found 14 | //ideally we should map the errors to an http not authorized error 15 | let cookie = request 16 | .cookie("auth-token") 17 | .expect("No auth token found in PasetoCookieIdentityPolicy"); 18 | //now grab the token from the cookie 19 | let token: &str = cookie.value(); 20 | 21 | //get the identity from the identity cookie 22 | let identity = request.get_identity().expect("Couldn't find identity"); 23 | let id = identity.as_str(); 24 | //get the paseto key from the shared state 25 | let key_val = request.app_data::>().unwrap().paseto_key.as_bytes(); 26 | //create a paseto key 27 | let key = PasetoSymmetricKey::::from(Key::from(key_val)); 28 | 29 | //attempt to parse the token when accessing a secure path, in practice this should also map to an HTTP error 30 | PasetoParser::::default() 31 | .set_implicit_assertion(ImplicitAssertion::from(id)) 32 | .parse(token, &key) 33 | .map_err(|err_val| println!("{}", err_val)) 34 | .expect("Couldn't validate authentication token"); 35 | println!( 36 | "Validated auth token in PasetoCookieIdentityPolicy\n for user {}\n", 37 | id 38 | ); 39 | Ok(Some(identity)) 40 | } 41 | 42 | impl IdentityPolicy for PasetoCookieIdentityPolicy { 43 | type Future = Ready, Error>>; 44 | type ResponseFuture = Ready>; 45 | 46 | fn from_request(&self, request: &mut ServiceRequest) -> Self::Future { 47 | ready(validate_auth_token(request)) 48 | } 49 | 50 | fn to_response( 51 | &self, 52 | _identity: Option, 53 | _changed: bool, 54 | _response: &mut ServiceResponse, 55 | ) -> Self::ResponseFuture { 56 | ready(Ok(())) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/actix_identity/readme.md: -------------------------------------------------------------------------------- 1 | # An example using a paseto token with the actix_identity crate 2 | 3 | This example creates a simple actix_web server using the default [CookieIdentityPolicy](https://docs.rs/actix-identity/latest/actix_identity/struct.CookieIdentityPolicy.html) as well as a custom [PasetoCookieIdentityPolicy](https://github.com/rrrodzilla/rusty_paseto/blob/main/examples/actix_identity/paseto.rs). The [CookieIdentityPolicy](https://docs.rs/actix-identity/latest/actix_identity/struct.CookieIdentityPolicy.html) is used for a an identity cookie. The latter policy is used to validate a PASETO authentication token that is stored in a separate cookie when the user logs in by making a POST request to the login endpoint. 4 | 5 | > **Warning**: Both cookies are created with `.secure(false)` so the example works when run over HTTP. In a real application you should enable the `secure` flag and provide proper error handling instead of panicking when authentication fails. 6 | 7 | When a user makes a request to a secure endpoint `/app/secure`, the [PasetoCookieIdentityPolicy](https://github.com/rrrodzilla/rusty_paseto/blob/main/examples/actix_identity/paseto.rs) validates the PASETO token on each request using the [CookieIdentityPolicy's](https://docs.rs/actix-identity/latest/actix_identity/struct.CookieIdentityPolicy.html) identity as the implicit assertion. This means if a user comes from a different device or changes their cookies after logging in, the implicit assertion will fail and the PASETO won't be validated. This example panics when this happens, but in practice you would map the error to a Not Authorized HTTP error. 8 | 9 | ## Usage 10 | 11 | First run `cargo run --example actix_identity" to build and start the web server. 12 | 13 | Then run the following command from a separate shell (I'm using [Fish](https://fishshell.com/)) to execute a series of requests that do the following: 14 | ```fish 15 | curl http://localhost:8080;curl -X POST http://localhost:8080/login -c ~/cookies; curl http://localhost:8080/app/secure -b ~/cookies; curl -X POST http://localhost:8080/logout -b ~/cookies 16 | ``` 17 | 18 | 1) Visits the site as an anonymous user and then, 19 | 2) Login the user, creating a paseto token and storing it in a cookie using the identity as the 20 | implicit assertion and then, 21 | 3) Logout the user, forgetting the identity 22 | 23 | 24 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "dev-environments": { 4 | "inputs": { 5 | "flake-parts": "flake-parts", 6 | "nixpkgs": "nixpkgs", 7 | "rust-overlay": "rust-overlay" 8 | }, 9 | "locked": { 10 | "lastModified": 1733502590, 11 | "narHash": "sha256-pyloxBUtHC1UkaZ49h7qA96egIk7yW/gkMQnXfuBCc0=", 12 | "owner": "Govcraft", 13 | "repo": "dev-environments", 14 | "rev": "43f911dc2dfc7549419d43e11e4a6e330f52ee85", 15 | "type": "github" 16 | }, 17 | "original": { 18 | "owner": "Govcraft", 19 | "repo": "dev-environments", 20 | "type": "github" 21 | } 22 | }, 23 | "flake-parts": { 24 | "inputs": { 25 | "nixpkgs-lib": "nixpkgs-lib" 26 | }, 27 | "locked": { 28 | "lastModified": 1733312601, 29 | "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", 30 | "owner": "hercules-ci", 31 | "repo": "flake-parts", 32 | "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "hercules-ci", 37 | "repo": "flake-parts", 38 | "type": "github" 39 | } 40 | }, 41 | "flake-parts_2": { 42 | "inputs": { 43 | "nixpkgs-lib": "nixpkgs-lib_2" 44 | }, 45 | "locked": { 46 | "lastModified": 1733312601, 47 | "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", 48 | "owner": "hercules-ci", 49 | "repo": "flake-parts", 50 | "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", 51 | "type": "github" 52 | }, 53 | "original": { 54 | "owner": "hercules-ci", 55 | "repo": "flake-parts", 56 | "type": "github" 57 | } 58 | }, 59 | "nixpkgs": { 60 | "locked": { 61 | "lastModified": 1733392399, 62 | "narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=", 63 | "owner": "NixOS", 64 | "repo": "nixpkgs", 65 | "rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "NixOS", 70 | "ref": "nixos-unstable", 71 | "repo": "nixpkgs", 72 | "type": "github" 73 | } 74 | }, 75 | "nixpkgs-lib": { 76 | "locked": { 77 | "lastModified": 1733096140, 78 | "narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=", 79 | "type": "tarball", 80 | "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" 81 | }, 82 | "original": { 83 | "type": "tarball", 84 | "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" 85 | } 86 | }, 87 | "nixpkgs-lib_2": { 88 | "locked": { 89 | "lastModified": 1733096140, 90 | "narHash": "sha256-1qRH7uAUsyQI7R1Uwl4T+XvdNv778H0Nb5njNrqvylY=", 91 | "type": "tarball", 92 | "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" 93 | }, 94 | "original": { 95 | "type": "tarball", 96 | "url": "https://github.com/NixOS/nixpkgs/archive/5487e69da40cbd611ab2cadee0b4637225f7cfae.tar.gz" 97 | } 98 | }, 99 | "nixpkgs_2": { 100 | "locked": { 101 | "lastModified": 1733392399, 102 | "narHash": "sha256-kEsTJTUQfQFIJOcLYFt/RvNxIK653ZkTBIs4DG+cBns=", 103 | "owner": "NixOS", 104 | "repo": "nixpkgs", 105 | "rev": "d0797a04b81caeae77bcff10a9dde78bc17f5661", 106 | "type": "github" 107 | }, 108 | "original": { 109 | "owner": "NixOS", 110 | "ref": "nixos-unstable", 111 | "repo": "nixpkgs", 112 | "type": "github" 113 | } 114 | }, 115 | "root": { 116 | "inputs": { 117 | "dev-environments": "dev-environments", 118 | "flake-parts": "flake-parts_2", 119 | "nixpkgs": "nixpkgs_2" 120 | } 121 | }, 122 | "rust-overlay": { 123 | "inputs": { 124 | "nixpkgs": [ 125 | "dev-environments", 126 | "nixpkgs" 127 | ] 128 | }, 129 | "locked": { 130 | "lastModified": 1733452419, 131 | "narHash": "sha256-eh2i2GtqdWVOP7yjiWtB8FMUWktCZ4vjo81n6g5mSiE=", 132 | "owner": "oxalica", 133 | "repo": "rust-overlay", 134 | "rev": "020701e6057992329a7cfafc6e3c5d5658bbcf79", 135 | "type": "github" 136 | }, 137 | "original": { 138 | "owner": "oxalica", 139 | "repo": "rust-overlay", 140 | "type": "github" 141 | } 142 | } 143 | }, 144 | "root": "root", 145 | "version": 7 146 | } 147 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Multi-environment project example"; 3 | inputs = { 4 | flake-parts.url = "github:hercules-ci/flake-parts"; 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | dev-environments.url = "github:Govcraft/dev-environments"; 7 | }; 8 | outputs = 9 | inputs@{ flake-parts, nixpkgs, ... }: 10 | flake-parts.lib.mkFlake { inherit inputs; } { 11 | imports = [ 12 | inputs.dev-environments.flakeModules.rust 13 | inputs.dev-environments.flakeModules.golang 14 | inputs.dev-environments.flakeModules.node 15 | inputs.dev-environments.flakeModules.typst 16 | ]; 17 | systems = [ 18 | "x86_64-linux" 19 | "aarch64-linux" 20 | "aarch64-darwin" 21 | "x86_64-darwin" 22 | ]; 23 | perSystem = 24 | { 25 | config, 26 | self', 27 | inputs', 28 | pkgs, 29 | system, 30 | ... 31 | }: 32 | { 33 | 34 | # Golang Development Environment Options 35 | # ---------------------------------- 36 | # enable: boolean - Enable/disable the Golang environment 37 | # goVersion: enum - Go toolchain version ("1.18", "1.19", "1.20", "1.21, "1.22"", "1.23") (default: "1.23") 38 | # withTools: list of strings - Additional Go tools to include (e.g., "golint", "gopls") 39 | # extraPackages: list of packages - Additional packages to include 40 | go-dev = { 41 | # enable = true; 42 | # goVersion = "1.23"; 43 | # withTools = [ "gopls" "golint" ]; 44 | # extraPackages = [ ]; 45 | }; 46 | 47 | # Rust Development Environment Options 48 | # ---------------------------------- 49 | # enable: boolean - Enable/disable the Rust environment 50 | # rustVersion: enum - Rust toolchain ("stable", "beta", "nightly") (default: "stable") 51 | # withTools: list of strings - Additional Rust tools to include (converted to cargo-*) 52 | # extraPackages: list of packages - Additional packages to include 53 | # ide.type: enum - IDE preference ("rust-rover", "vscode", "none") (default: "none") 54 | rust-dev = { 55 | enable = true; 56 | # rustVersion = "nightly"; 57 | # Example configuration: 58 | withTools = [ "outdated" ]; # Will be prefixed with cargo- 59 | # extraPackages = [ ]; 60 | # ide.type = "none"; 61 | }; 62 | 63 | # Node.js Development Environment Options 64 | # ------------------------------------- 65 | # enable: boolean - Enable/disable the Node environment 66 | # nodeVersion: string - Version of Node.js to use (default: "20") 67 | # withTools: list of strings - Global tools to include (default: ["typescript" "yarn" "pnpm"]) 68 | # extraPackages: list of packages - Additional packages to include 69 | # ide.type: enum - IDE preference ("vscode", "webstorm", "none") (default: "none") 70 | node-dev = { 71 | # Example configuration: 72 | # enable = true; 73 | # nodeVersion = "20"; 74 | # withTools = [ "typescript" "yarn" "pnpm" ]; 75 | # extraPackages = [ ]; 76 | # ide.type = "none"; 77 | }; 78 | 79 | typst-dev = { 80 | # Example configuration: 81 | # enable = true; 82 | # withTools = [ "typst-fmt" "typst-lsp" ]; 83 | # extraPackages = [ ]; 84 | # ide.type = "none"; 85 | }; 86 | # Create the combined shell 87 | devShells.default = pkgs.mkShell { 88 | buildInputs = nixpkgs.lib.flatten (nixpkgs.lib.attrValues config.env-packages ++ [ ]); 89 | shellHook = nixpkgs.lib.concatStringsSep "\n" (nixpkgs.lib.attrValues config.env-hooks); 90 | }; 91 | }; 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /funding.yml: -------------------------------------------------------------------------------- 1 | github: rrrodzilla 2 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | tab_spaces = 2 2 | max_width = 120 3 | imports_granularity = "Crate" 4 | use_field_init_shorthand = true 5 | merge_imports = true 6 | 7 | -------------------------------------------------------------------------------- /src/core/common/authentication_key.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use std::ops::Deref; 3 | 4 | pub(crate) struct AuthenticationKey { 5 | pub(crate) version: PhantomData, 6 | pub(crate) purpose: PhantomData, 7 | pub(crate) key: Vec, 8 | } 9 | 10 | impl AsRef<[u8]> for AuthenticationKey { 11 | fn as_ref(&self) -> &[u8] { 12 | &self.key 13 | } 14 | } 15 | 16 | impl Deref for AuthenticationKey { 17 | type Target = [u8]; 18 | 19 | fn deref(&self) -> &Self::Target { 20 | &self.key 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/core/common/authentication_key_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod v1_local; 2 | mod v3_local; 3 | mod v4_local; -------------------------------------------------------------------------------- /src/core/common/authentication_key_impl/v1_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v1_local")] 2 | use std::marker::PhantomData; 3 | use ring::hkdf; 4 | use crate::core::{Local, PasetoError, PasetoNonce, PasetoSymmetricKey, V1}; 5 | use crate::core::common::authentication_key::AuthenticationKey; 6 | use crate::core::common::hkdf_key::HkdfKey; 7 | 8 | impl AuthenticationKey { 9 | pub(crate) fn try_from( 10 | message: &[u8; 24], 11 | key: &PasetoSymmetricKey, 12 | nonce: &PasetoNonce, 13 | ) -> Result { 14 | let info = message.as_ref(); 15 | let salt = hkdf::Salt::new(hkdf::HKDF_SHA384, &nonce[..16]); 16 | let HkdfKey(out) = salt.extract(key.as_ref()).expand(&[info], HkdfKey(32))?.try_into()?; 17 | 18 | Ok(Self { 19 | version: PhantomData, 20 | purpose: PhantomData, 21 | key: out, 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/core/common/authentication_key_impl/v3_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v3_local")] 2 | use std::marker::PhantomData; 3 | use ring::hkdf; 4 | use crate::core::{Key, Local, PasetoError, PasetoSymmetricKey, V3}; 5 | use crate::core::common::HkdfKey; 6 | 7 | impl crate::core::common::authentication_key::AuthenticationKey { 8 | pub(crate) fn try_from(message: &Key<56>, key: &PasetoSymmetricKey) -> Result { 9 | let info = message.as_ref(); 10 | let salt = hkdf::Salt::new(hkdf::HKDF_SHA384, &[]); 11 | let HkdfKey(out) = salt.extract(key.as_ref()).expand(&[info], HkdfKey(48))?.try_into()?; 12 | 13 | Ok(Self { 14 | version: PhantomData, 15 | purpose: PhantomData, 16 | key: out, 17 | }) 18 | } 19 | } -------------------------------------------------------------------------------- /src/core/common/authentication_key_impl/v4_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v4_local")] 2 | use std::marker::PhantomData; 3 | use std::ops::Deref; 4 | use blake2::digest::consts::U32; 5 | use blake2::{Blake2bMac, digest::Update}; 6 | use blake2::digest::FixedOutput; 7 | use digest::KeyInit; 8 | use crate::core::{Key, Local, PasetoSymmetricKey, V4}; 9 | 10 | impl crate::core::common::authentication_key::AuthenticationKey { 11 | pub(crate) fn from(message: &Key<56>, key: &PasetoSymmetricKey) -> Self { 12 | let mut context = Blake2bMac::::new_from_slice(key.as_ref()).unwrap(); 13 | context.update(message.as_ref()); 14 | let binding = context.finalize_fixed(); 15 | let key = binding.to_vec(); 16 | Self { 17 | version: PhantomData, 18 | purpose: PhantomData, 19 | key, 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/core/common/authentication_key_separator.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Display; 3 | use std::ops::{Add, Deref}; 4 | use crate::core::{Key, Local, PasetoNonce}; 5 | 6 | #[derive(Debug)] 7 | pub (crate) struct AuthenticationKeySeparator(&'static str); 8 | 9 | impl Display for AuthenticationKeySeparator { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | write!(f, "{}", &self.0) 12 | } 13 | } 14 | 15 | impl Default for AuthenticationKeySeparator { 16 | fn default() -> Self { 17 | Self("paseto-auth-key-for-aead") 18 | } 19 | } 20 | 21 | impl Deref for AuthenticationKeySeparator { 22 | type Target = [u8]; 23 | 24 | fn deref(&self) -> &Self::Target { 25 | self.0.as_bytes() 26 | } 27 | } 28 | 29 | impl AsRef for AuthenticationKeySeparator { 30 | fn as_ref(&self) -> &str { 31 | self.0 32 | } 33 | } 34 | 35 | impl<'a, Version> Add<&PasetoNonce<'a, Version, Local>> for AuthenticationKeySeparator { 36 | type Output = Key<56>; 37 | 38 | fn add(self, rhs: &PasetoNonce) -> Self::Output { 39 | let mut output = [0u8; 56]; 40 | output[..24].copy_from_slice(self.0.as_bytes()); 41 | output[24..].copy_from_slice(rhs.as_ref()); 42 | Key::<56>::from(output) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/common/cipher_text.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | pub(crate) struct CipherText { 4 | pub(crate) ciphertext: Vec, 5 | pub(crate) version: PhantomData, 6 | pub(crate) purpose: PhantomData, 7 | } 8 | 9 | impl AsRef> for CipherText { 10 | fn as_ref(&self) -> &Vec { 11 | &self.ciphertext 12 | } 13 | } 14 | 15 | impl std::ops::Deref for CipherText { 16 | type Target = Vec; 17 | fn deref(&self) -> &Self::Target { 18 | &self.ciphertext 19 | } 20 | } -------------------------------------------------------------------------------- /src/core/common/cipher_text_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod v1_public; 2 | mod v1_local; 3 | mod v2_local; 4 | mod v3_local; 5 | mod v4_local; -------------------------------------------------------------------------------- /src/core/common/cipher_text_impl/v1_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v1_local")] 2 | use std::marker::PhantomData; 3 | use aes::Aes256Ctr; 4 | use aes::cipher::generic_array::GenericArray; 5 | use aes::cipher::{NewCipher, StreamCipher}; 6 | use crate::core::common::cipher_text::CipherText; 7 | use crate::core::{Local, V1}; 8 | use crate::core::common::EncryptionKey; 9 | 10 | impl CipherText { 11 | pub(crate) fn from(payload: &[u8], encryption_key: &EncryptionKey) -> Self { 12 | let key = GenericArray::from_slice(encryption_key.as_ref()); 13 | let nonce = GenericArray::from_slice(encryption_key.counter_nonce()); 14 | let mut cipher = Aes256Ctr::new(key, nonce); 15 | let mut ciphertext = vec![0u8; payload.as_ref().len()]; 16 | 17 | ciphertext.copy_from_slice(payload); 18 | 19 | cipher.apply_keystream(&mut ciphertext); 20 | 21 | CipherText { 22 | ciphertext, 23 | version: PhantomData, 24 | purpose: PhantomData, 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/core/common/cipher_text_impl/v1_public.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v1_public")] 2 | use std::marker::PhantomData; 3 | use ring::signature::{RSA_PSS_2048_8192_SHA384, UnparsedPublicKey}; 4 | use crate::core::common::{CipherText, PreAuthenticationEncoding}; 5 | use crate::core::{Footer, Header, PasetoError, Public, V1}; 6 | 7 | impl CipherText { 8 | pub(crate) fn try_verify(decoded_payload: &[u8], public_key: &impl AsRef<[u8]>, footer: &Footer) -> Result { 9 | let signature = decoded_payload[(decoded_payload.len() - 256)..].as_ref(); 10 | let public_key = UnparsedPublicKey::new(&RSA_PSS_2048_8192_SHA384, public_key); 11 | let msg = decoded_payload[..(decoded_payload.len() - 256)].as_ref(); 12 | 13 | let pae = PreAuthenticationEncoding::parse(&[&Header::::default(), msg, footer]); 14 | 15 | public_key.verify(&pae, signature)?; 16 | 17 | let ciphertext = Vec::from(msg); 18 | 19 | Ok(CipherText { 20 | ciphertext, 21 | version: PhantomData, 22 | purpose: PhantomData, 23 | }) 24 | } 25 | } -------------------------------------------------------------------------------- /src/core/common/cipher_text_impl/v2_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v2_local")] 2 | use std::marker::PhantomData; 3 | use chacha20poly1305::{KeyInit, XChaCha20Poly1305, XNonce}; 4 | use chacha20poly1305::aead::{Aead, Payload}; 5 | use crate::core::common::{CipherText, PreAuthenticationEncoding}; 6 | use crate::core::{Local, PasetoError, PasetoSymmetricKey, V2}; 7 | 8 | impl CipherText { 9 | pub(crate) fn try_decrypt_from( 10 | key: &PasetoSymmetricKey, 11 | nonce: &XNonce, 12 | payload: &[u8], 13 | pre_auth: &PreAuthenticationEncoding, 14 | ) -> Result { 15 | //let ciphertext = CipherText::try_from(&key, &nonce, &payload, &pae)?; 16 | 17 | let aead = XChaCha20Poly1305::new_from_slice(key.as_ref()).map_err(|_| PasetoError::Cryption)?; 18 | //encrypt cipher_text 19 | let ciphertext = aead 20 | .decrypt( 21 | nonce, 22 | Payload { 23 | msg: payload, 24 | aad: pre_auth.as_ref(), 25 | }, 26 | ) 27 | .map_err(|_| PasetoError::ChaChaCipherError)?; 28 | 29 | Ok(CipherText { 30 | ciphertext, 31 | version: PhantomData, 32 | purpose: PhantomData, 33 | }) 34 | } 35 | 36 | pub(crate) fn try_from( 37 | key: &PasetoSymmetricKey, 38 | nonce: &XNonce, 39 | payload: &[u8], 40 | pre_auth: &PreAuthenticationEncoding, 41 | ) -> Result { 42 | let aead = XChaCha20Poly1305::new_from_slice(key.as_ref()).map_err(|_| PasetoError::Cryption)?; 43 | //encrypt cipher_text 44 | let ciphertext = aead 45 | .encrypt( 46 | nonce, 47 | Payload { 48 | msg: payload, 49 | aad: pre_auth.as_ref(), 50 | }, 51 | ) 52 | .map_err(|_| PasetoError::ChaChaCipherError)?; 53 | 54 | Ok(CipherText { 55 | ciphertext, 56 | version: PhantomData, 57 | purpose: PhantomData, 58 | }) 59 | } 60 | } -------------------------------------------------------------------------------- /src/core/common/cipher_text_impl/v3_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v3_local")] 2 | use std::marker::PhantomData; 3 | use aes::Aes256Ctr; 4 | use aes::cipher::generic_array::GenericArray; 5 | use aes::cipher::{NewCipher, StreamCipher}; 6 | use crate::core::common::{CipherText, EncryptionKey}; 7 | use crate::core::{Local, V3}; 8 | 9 | impl CipherText { 10 | pub(crate) fn from(payload: &[u8], encryption_key: &EncryptionKey) -> Self { 11 | let key = GenericArray::from_slice(encryption_key.as_ref()); 12 | let nonce = GenericArray::from_slice(encryption_key.counter_nonce()); 13 | let mut cipher = Aes256Ctr::new(key, nonce); 14 | let mut ciphertext = vec![0u8; payload.len()]; 15 | 16 | ciphertext.copy_from_slice(payload); 17 | 18 | cipher.apply_keystream(&mut ciphertext); 19 | 20 | CipherText { 21 | ciphertext, 22 | version: PhantomData, 23 | purpose: PhantomData, 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/core/common/cipher_text_impl/v4_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v4_local")] 2 | use std::marker::PhantomData; 3 | use chacha20::cipher::{KeyIvInit, StreamCipher}; 4 | use crate::core::common::{CipherText, EncryptionKey}; 5 | use crate::core::{Local, V4}; 6 | 7 | impl CipherText { 8 | pub(crate) fn from(payload: &[u8], encryption_key: &EncryptionKey) -> Self { 9 | let mut ciphertext = vec![0u8; payload.len()]; 10 | ciphertext.copy_from_slice(payload); 11 | 12 | let n2 = encryption_key.counter_nonce(); 13 | let mut cipher = chacha20::XChaCha20::new(encryption_key.as_ref(), n2); 14 | cipher.apply_keystream(&mut ciphertext); 15 | 16 | CipherText { 17 | ciphertext, 18 | version: PhantomData, 19 | purpose: PhantomData, 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/core/common/encryption_key.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | #[derive(Default)] 4 | pub(crate) struct EncryptionKey { 5 | pub(crate) version: PhantomData, 6 | pub(crate) purpose: PhantomData, 7 | pub(crate) key: Vec, 8 | #[cfg(any(feature = "v1_local", feature = "v3_local", feature = "v4_local"))] 9 | pub(crate) nonce: Vec, 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/core/common/encryption_key_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod v1_local; 2 | mod v3_local; 3 | mod v4_local; 4 | mod v_local; -------------------------------------------------------------------------------- /src/core/common/encryption_key_impl/v1_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v1_local")] 2 | use std::marker::PhantomData; 3 | use ring::hkdf; 4 | use crate::core::common::EncryptionKey; 5 | use crate::core::{Key, Local, PasetoError, PasetoNonce, PasetoSymmetricKey, V1}; 6 | use crate::core::common::hkdf_key::HkdfKey; 7 | impl EncryptionKey { 8 | pub(crate) fn try_from( 9 | message: &Key<21>, 10 | key: &PasetoSymmetricKey, 11 | nonce: &PasetoNonce, 12 | ) -> Result { 13 | let info = message.as_ref(); 14 | let salt = hkdf::Salt::new(hkdf::HKDF_SHA384, &nonce[..16]); 15 | let HkdfKey(out) = salt.extract(key.as_ref()).expand(&[info], HkdfKey(32))?.try_into()?; 16 | 17 | Ok(Self { 18 | version: PhantomData, 19 | purpose: PhantomData, 20 | key: out.to_vec(), 21 | nonce: nonce[16..].to_vec(), 22 | }) 23 | } 24 | 25 | pub(crate) fn counter_nonce(&self) -> &Vec { 26 | &self.nonce 27 | } 28 | } -------------------------------------------------------------------------------- /src/core/common/encryption_key_impl/v3_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v3_local")] 2 | use std::marker::PhantomData; 3 | use ring::hkdf; 4 | use crate::core::{Key, Local, PasetoError, PasetoSymmetricKey, V3}; 5 | use crate::core::common::{EncryptionKey, HkdfKey}; 6 | impl EncryptionKey { 7 | pub(crate) fn try_from(message: &Key<53>, key: &PasetoSymmetricKey) -> Result { 8 | let info = message.as_ref(); 9 | let salt = hkdf::Salt::new(hkdf::HKDF_SHA384, &[]); 10 | 11 | let HkdfKey(out) = salt.extract(key.as_ref()).expand(&[info], HkdfKey(48))?.try_into()?; 12 | 13 | Ok(Self { 14 | version: PhantomData, 15 | purpose: PhantomData, 16 | key: out[..32].to_vec(), 17 | nonce: out[32..].to_vec(), 18 | }) 19 | } 20 | 21 | pub(crate) fn counter_nonce(&self) -> &Vec { 22 | &self.nonce 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/core/common/encryption_key_impl/v4_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v4_local")] 2 | use std::marker::PhantomData; 3 | use std::ops::Deref; 4 | use blake2::digest::consts::U56; 5 | use blake2::{Blake2bMac, digest::Update}; 6 | use blake2::digest::FixedOutput; 7 | use digest::KeyInit; 8 | use chacha20::{XNonce, Key}; 9 | use crate::core::common::EncryptionKey; 10 | use crate::core::{Local, PasetoSymmetricKey, V4}; 11 | 12 | impl EncryptionKey { 13 | pub(crate) fn from(message: &crate::core::Key<53>, key: &PasetoSymmetricKey) -> Self { 14 | let mut context = Blake2bMac::::new_from_slice(key.as_ref()).unwrap(); 15 | context.update(message.as_ref()); 16 | let binding = context.finalize_fixed(); 17 | let context = binding.to_vec(); 18 | let key = context[..32].to_vec(); 19 | let nonce = context[32..56].to_vec(); 20 | 21 | assert_eq!(key.len(), 32); 22 | assert_eq!(nonce.len(), 24); 23 | Self { 24 | key, 25 | nonce, 26 | version: PhantomData, 27 | purpose: PhantomData, 28 | } 29 | 30 | } 31 | pub(crate) fn counter_nonce(&self) -> &XNonce { 32 | XNonce::from_slice(&self.nonce) 33 | } 34 | } 35 | 36 | impl AsRef for EncryptionKey { 37 | fn as_ref(&self) -> &Key { 38 | Key::from_slice(&self.key) 39 | } 40 | } 41 | 42 | impl Deref for EncryptionKey { 43 | type Target = [u8]; 44 | 45 | fn deref(&self) -> &Self::Target { 46 | Key::from_slice(&self.key) 47 | } 48 | } -------------------------------------------------------------------------------- /src/core/common/encryption_key_impl/v_local.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use crate::core::common::EncryptionKey; 3 | use crate::core::{Local, V1orV3}; 4 | 5 | impl AsRef> for EncryptionKey 6 | where 7 | Version: V1orV3, 8 | { 9 | fn as_ref(&self) -> &Vec { 10 | &self.key 11 | } 12 | } 13 | 14 | impl Deref for EncryptionKey 15 | where 16 | Version: V1orV3, 17 | { 18 | type Target = [u8]; 19 | 20 | fn deref(&self) -> &Self::Target { 21 | &self.key 22 | } 23 | } -------------------------------------------------------------------------------- /src/core/common/encryption_key_separator.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Display; 3 | use std::ops::{Add, Deref}; 4 | use crate::core::{Key, Local, PasetoNonce}; 5 | 6 | #[derive(Debug)] 7 | pub (crate) struct EncryptionKeySeparator(&'static str); 8 | 9 | impl Display for EncryptionKeySeparator { 10 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 11 | write!(f, "{}", &self.0) 12 | } 13 | } 14 | 15 | impl Default for EncryptionKeySeparator { 16 | fn default() -> Self { 17 | Self("paseto-encryption-key") 18 | } 19 | } 20 | 21 | impl Deref for EncryptionKeySeparator { 22 | type Target = [u8]; 23 | 24 | fn deref(&self) -> &Self::Target { 25 | self.0.as_bytes() 26 | } 27 | } 28 | 29 | impl AsRef for EncryptionKeySeparator { 30 | fn as_ref(&self) -> &str { 31 | self.0 32 | } 33 | } 34 | 35 | impl<'a, Version> Add<&PasetoNonce<'a, Version, Local>> for EncryptionKeySeparator { 36 | type Output = Key<53>; 37 | 38 | fn add(self, rhs: &PasetoNonce) -> Self::Output { 39 | let mut output = [0u8; 53]; 40 | output[..21].copy_from_slice(self.0.as_bytes()); 41 | output[21..].copy_from_slice(rhs.as_ref()); 42 | Key::<53>::from(output) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/common/encryption_nonce.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "chacha20poly1305")] 2 | use chacha20poly1305::XNonce; 3 | 4 | #[cfg(feature = "chacha20poly1305")] 5 | struct EncryptionNonce(XNonce); 6 | 7 | #[cfg(feature = "chacha20poly1305")] 8 | impl AsRef for EncryptionNonce { 9 | fn as_ref(&self) -> &XNonce { 10 | &self.0 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/core/common/hkdf_key.rs: -------------------------------------------------------------------------------- 1 | use ring::hkdf; 2 | use crate::core::PasetoError; 3 | 4 | #[derive(Debug, PartialEq)] 5 | pub(crate) struct HkdfKey(pub T); 6 | 7 | impl hkdf::KeyType for HkdfKey { 8 | fn len(&self) -> usize { 9 | self.0 10 | } 11 | } 12 | 13 | impl TryFrom>> for HkdfKey> { 14 | type Error = PasetoError; 15 | fn try_from(okm: hkdf::Okm>) -> Result { 16 | let mut r = vec![0u8; okm.len().0]; 17 | okm.fill(&mut r)?; 18 | Ok(Self(r)) 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/core/common/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | pub(crate) mod cipher_text; 3 | mod encryption_key; 4 | mod encryption_nonce; 5 | mod tag; 6 | mod raw_payload; 7 | mod authentication_key; 8 | mod authentication_key_separator; 9 | mod encryption_key_separator; 10 | mod pre_authentication_encoding; 11 | mod hkdf_key; 12 | mod encryption_key_impl; 13 | mod tag_impl; 14 | mod raw_payload_impl; 15 | mod authentication_key_impl; 16 | mod cipher_text_impl; 17 | 18 | pub(crate) use encryption_key::EncryptionKey; 19 | pub(crate) use raw_payload::RawPayload; 20 | pub(crate) use pre_authentication_encoding::PreAuthenticationEncoding; 21 | pub(crate) use cipher_text::CipherText; 22 | pub(crate) use authentication_key::AuthenticationKey; 23 | pub(crate) use authentication_key_separator::AuthenticationKeySeparator; 24 | pub(crate) use encryption_key_separator::EncryptionKeySeparator; 25 | pub(crate) use tag::Tag; 26 | pub(crate) use hkdf_key::HkdfKey; 27 | -------------------------------------------------------------------------------- /src/core/common/pre_authentication_encoding.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | pub struct PreAuthenticationEncoding(Vec); 4 | 5 | /// Performs Pre-Authentication Encoding (or PAE) as described in the 6 | /// Paseto Specification v2. 7 | /// 8 | impl PreAuthenticationEncoding { 9 | /// * `pieces` - The Pieces to concatenate, and encode together. 10 | /// Refactored from original code found at 11 | /// 12 | pub fn parse<'a>(pieces: &'a [&'a [u8]]) -> Self { 13 | let the_vec = PreAuthenticationEncoding::le64(pieces.len() as u64); 14 | 15 | Self(pieces.iter().fold(the_vec, |mut acc, piece| { 16 | acc.extend(PreAuthenticationEncoding::le64(piece.len() as u64)); 17 | acc.extend(piece.iter()); 18 | acc 19 | })) 20 | } 21 | /// Encodes a u64-bit unsigned integer into a little-endian binary string. 22 | /// 23 | /// * `to_encode` - The u8 to encode. 24 | /// Copied and gently refactored from 25 | pub(crate) fn le64(mut to_encode: u64) -> Vec { 26 | let mut the_vec = Vec::with_capacity(8); 27 | 28 | for _idx in 0..8 { 29 | the_vec.push((to_encode & 255) as u8); 30 | to_encode >>= 8; 31 | } 32 | 33 | the_vec 34 | } 35 | } 36 | 37 | impl Deref for PreAuthenticationEncoding { 38 | type Target = [u8]; 39 | 40 | fn deref(&self) -> &Self::Target { 41 | self.0.deref() 42 | } 43 | } 44 | 45 | impl AsRef> for PreAuthenticationEncoding { 46 | fn as_ref(&self) -> &Vec { 47 | &self.0 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/core/common/raw_payload.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | pub struct RawPayload { 4 | version: PhantomData, 5 | purpose: PhantomData, 6 | } 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/core/common/raw_payload_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod nist_local; 2 | mod v2_local; 3 | mod v4_local; 4 | mod v_public; -------------------------------------------------------------------------------- /src/core/common/raw_payload_impl/nist_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(feature = "v1_local", feature = "v3_local"))] 2 | use base64::prelude::*; 3 | use crate::core::common::RawPayload; 4 | use crate::core::{Local, PasetoError, PasetoNonce, V1orV3}; 5 | 6 | impl RawPayload 7 | where 8 | Version: V1orV3, 9 | { 10 | pub(crate) fn from( 11 | nonce: &PasetoNonce, 12 | ciphertext: &impl AsRef>, 13 | tag: &impl AsRef<[u8]>, 14 | ) -> Result { 15 | let tag_len = tag.as_ref().len(); 16 | let concat_len: usize = match (nonce.len() + tag_len).checked_add(ciphertext.as_ref().len()) { 17 | Some(len) => len, 18 | None => return Err(PasetoError::Signature), 19 | }; 20 | 21 | let mut raw_token = vec![0u8; concat_len]; 22 | raw_token[..nonce.as_ref().len()].copy_from_slice(nonce.as_ref()); 23 | raw_token[nonce.as_ref().len()..nonce.as_ref().len() + ciphertext.as_ref().len()] 24 | .copy_from_slice(ciphertext.as_ref()); 25 | raw_token[concat_len - tag_len..].copy_from_slice(tag.as_ref()); 26 | 27 | Ok(BASE64_URL_SAFE_NO_PAD.encode(&raw_token)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/common/raw_payload_impl/v2_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v2_local")] 2 | use base64::prelude::*; 3 | use crate::core::common::RawPayload; 4 | use crate::core::{Local, V2}; 5 | 6 | impl RawPayload { 7 | pub(crate) fn from(blake2_hash: &[u8], ciphertext: &[u8]) -> String { 8 | let mut raw_token = Vec::new(); 9 | raw_token.extend_from_slice(blake2_hash); 10 | raw_token.extend_from_slice(ciphertext); 11 | 12 | BASE64_URL_SAFE_NO_PAD.encode(&raw_token) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/core/common/raw_payload_impl/v4_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v4_local")] 2 | use base64::prelude::*; 3 | use crate::core::common::RawPayload; 4 | use crate::core::{Local, PasetoError, PasetoNonce, V4}; 5 | 6 | impl RawPayload { 7 | pub(crate) fn try_from( 8 | nonce: &PasetoNonce, 9 | ciphertext: &impl AsRef>, 10 | tag: &impl AsRef<[u8]>, 11 | ) -> Result { 12 | let tag_len = tag.as_ref().len(); 13 | let concat_len: usize = match (nonce.len() + tag_len).checked_add(ciphertext.as_ref().len()) { 14 | Some(len) => len, 15 | None => return Err(PasetoError::Cryption), 16 | }; 17 | 18 | let mut raw_token = vec![0u8; concat_len]; 19 | raw_token[..nonce.as_ref().len()].copy_from_slice(nonce.as_ref()); 20 | raw_token[nonce.as_ref().len()..nonce.as_ref().len() + ciphertext.as_ref().len()] 21 | .copy_from_slice(ciphertext.as_ref()); 22 | raw_token[concat_len - tag_len..].copy_from_slice(tag.as_ref()); 23 | 24 | Ok(BASE64_URL_SAFE_NO_PAD.encode(&raw_token)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/core/common/raw_payload_impl/v_public.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(feature = "v1_public", feature = "v2_public", feature = "v3_public", feature = "v4_public"))] 2 | use base64::prelude::*; 3 | use crate::core::common::RawPayload; 4 | use crate::core::Public; 5 | 6 | impl RawPayload { 7 | pub(crate) fn from(payload: &[u8], signature: &impl AsRef<[u8]>) -> String { 8 | let mut raw_token = Vec::from(payload); 9 | raw_token.extend_from_slice(signature.as_ref()); 10 | 11 | BASE64_URL_SAFE_NO_PAD.encode(&raw_token) 12 | } 13 | } -------------------------------------------------------------------------------- /src/core/common/tag.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | use std::ops::Deref; 3 | 4 | pub(crate) struct Tag { 5 | pub(crate) version: PhantomData, 6 | pub(crate) purpose: PhantomData, 7 | pub(crate) tag: Vec, 8 | } 9 | 10 | 11 | 12 | impl AsRef<[u8]> for Tag { 13 | fn as_ref(&self) -> &[u8] { 14 | &self.tag 15 | } 16 | } 17 | 18 | impl Deref for Tag { 19 | type Target = [u8]; 20 | fn deref(&self) -> &Self::Target { 21 | &self.tag 22 | } 23 | } -------------------------------------------------------------------------------- /src/core/common/tag_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod v4_local; 2 | mod nist_local; -------------------------------------------------------------------------------- /src/core/common/tag_impl/nist_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(feature = "v1_local", feature = "v3_local"))] 2 | use std::marker::PhantomData; 3 | use hmac::{Hmac, Mac}; 4 | use crate::core::{Local, V1orV3}; 5 | use crate::core::common::PreAuthenticationEncoding; 6 | 7 | impl crate::core::common::tag::Tag 8 | where 9 | Version: V1orV3, 10 | { 11 | pub(crate) fn from(authentication_key: impl AsRef<[u8]>, pae: &PreAuthenticationEncoding) -> Self { 12 | type HmacSha384 = Hmac; 13 | 14 | let mut mac = HmacSha384::new_from_slice(authentication_key.as_ref()).expect("HMAC can take key of any size"); 15 | mac.update(pae.as_ref()); 16 | 17 | let out = mac.finalize(); 18 | 19 | Self { 20 | tag: out.into_bytes().to_vec(), 21 | version: PhantomData, 22 | purpose: PhantomData, 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/core/common/tag_impl/v4_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v4_local")] 2 | use std::marker::PhantomData; 3 | use std::ops::Deref; 4 | use blake2::digest::consts::U32; 5 | use blake2::{Blake2bMac, digest::Update}; 6 | use blake2::digest::FixedOutput; 7 | use digest::KeyInit; 8 | use crate::core::common::PreAuthenticationEncoding; 9 | use crate::core::{Local, V4}; 10 | 11 | impl crate::core::common::tag::Tag { 12 | pub(crate) fn from(authentication_key: impl AsRef<[u8]>, pae: &PreAuthenticationEncoding) -> Self { 13 | let mut tag_context = Blake2bMac::::new_from_slice(authentication_key.as_ref()).unwrap(); 14 | tag_context.update(pae.as_ref()); 15 | let binding = tag_context.finalize_fixed(); 16 | let tag = binding.to_vec(); 17 | Self { 18 | tag, 19 | version: PhantomData, 20 | purpose: PhantomData, 21 | } 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/core/error.rs: -------------------------------------------------------------------------------- 1 | use std::array::TryFromSliceError; 2 | use thiserror::Error; 3 | 4 | /// Potential errors from attempting to build a token claim 5 | #[derive(Debug, Error)] 6 | pub enum PasetoError { 7 | ///A general, unspecified (for security reasons) cipher error 8 | #[error("A cipher error occurred")] 9 | PasetoCipherError(Box), 10 | ///A general, unspecified (for security reasons) cipher error 11 | #[error("An unspecified cryption error occured")] 12 | Cryption, 13 | ///A problem generating a signature 14 | #[error("Key was not in the correct format")] 15 | InvalidKey, 16 | ///A problem generating a signature 17 | #[error("Could not assemble final signature.")] 18 | Signature, 19 | /// Occurs when a private RSA key is not in pkcs#8 format 20 | #[error("A private RSA key was not in the correct format")] 21 | KeyRejected { 22 | ///Surfaces key rejection errors from ring 23 | #[from] 24 | source: ring::error::KeyRejected, 25 | }, 26 | ///A general, unspecified (for security reasons) cipher error 27 | #[error("An unspecified cipher error occurred")] 28 | Cipher { 29 | ///Surfaces unspecified errors from ring 30 | #[from] 31 | source: ring::error::Unspecified, 32 | }, 33 | #[cfg(feature = "ed25519-dalek")] 34 | ///An RSA cipher error 35 | #[error("An unspecified cipher error occurred")] 36 | RsaCipher { 37 | ///An RSA cipher error 38 | #[from] 39 | source: ed25519_dalek::ed25519::Error, 40 | }, 41 | #[cfg(feature = "p384")] 42 | ///An ECSDA cipher error 43 | #[error("An unspecified ECSDA error occurred")] 44 | ECSDAError { 45 | ///An ECSDA cipher error 46 | #[from] 47 | source: p384::ecdsa::Error, 48 | }, 49 | #[cfg(feature = "blake2")] 50 | ///An RSA cipher error 51 | #[error("An unspecified cipher error occurred")] 52 | InvalidLength { 53 | ///An RSA cipher error 54 | #[from] 55 | source: blake2::digest::InvalidLength, 56 | }, 57 | ///Occurs when a signature fails verification 58 | #[error("The token signature could not be verified")] 59 | InvalidSignature, 60 | #[error("A slice conversion error occurred")] 61 | TryFromSlice { 62 | ///Surfaces errors from slice conversion attempts 63 | #[from] 64 | source: TryFromSliceError, 65 | }, 66 | ///Occurs when an untrusted token string is unable to be parsed into its constituent parts 67 | #[error("This string has an incorrect number of parts and cannot be parsed into a token")] 68 | IncorrectSize, 69 | ///Occurs when an incorrect header is provided on an untrusted token string 70 | #[error("The token header is invalid")] 71 | WrongHeader, 72 | ///Occurs when an incorrect footer was passed in an attempt to parse an untrusted token string 73 | #[error("The provided footer is invalid")] 74 | FooterInvalid, 75 | ///Occurs when a base64 encoded payload cannot be decoded 76 | #[error("A base64 decode error occurred")] 77 | PayloadBase64Decode { 78 | ///Surfaced from the base64 crate 79 | #[from] 80 | source: base64::DecodeError, 81 | }, 82 | ///Occurs when a string fails parsing as Utf8 83 | #[error("A Utf8 parsing error occurred")] 84 | Utf8Error { 85 | ///Surfaced from std::str::Utf8 86 | #[from] 87 | source: std::str::Utf8Error, 88 | }, 89 | ///A cipher error from the ChaCha algorithm 90 | #[error("An unspecified cipher error occurred")] 91 | ChaChaCipherError, 92 | ///An infallible error 93 | #[error("A Utf8 parsing error occurred")] 94 | Infallibale { 95 | ///An infallible error 96 | #[from] 97 | source: std::convert::Infallible, 98 | }, 99 | ///Occurs when a string fails conversion from Utf8 100 | #[error("A Utf8 parsing error occurred")] 101 | FromUtf8Error { 102 | ///Surfaced from std::string::FromUtf8Error 103 | #[from] 104 | source: std::string::FromUtf8Error, 105 | }, 106 | } 107 | -------------------------------------------------------------------------------- /src/core/footer.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::fmt; 3 | use std::ops::Deref; 4 | 5 | /// Unencrypted text, potentially JSON or some other structured format, typically used for key rotation schemes, packed into the 6 | /// payload as part of the cipher scheme. 7 | /// 8 | /// # Usage 9 | /// ``` 10 | /// # #[cfg(feature = "default")] 11 | /// # { 12 | /// # use rusty_paseto::prelude::*; 13 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::from(b"wubbalubbadubdubwubbalubbadubdub")); 14 | /// let token = PasetoBuilder::::default() 15 | /// // note how we set the footer here 16 | /// .set_footer(Footer::from("Sometimes science is more art than science")) 17 | /// .build(&key)?; 18 | /// 19 | /// // the footer same footer should be used to parse the token 20 | /// let json_value = PasetoParser::::default() 21 | /// .set_footer(Footer::from("Sometimes science is more art than science")) 22 | /// .parse(&token, &key)?; 23 | /// # } 24 | /// # Ok::<(),anyhow::Error>(()) 25 | /// ``` 26 | #[derive(Default, Debug, Clone, Copy)] 27 | pub struct Footer<'a>(&'a str); 28 | 29 | impl<'a> Base64Encodable for Footer<'a> {} 30 | 31 | impl<'a> Deref for Footer<'a> { 32 | type Target = [u8]; 33 | 34 | fn deref(&self) -> &'a Self::Target { 35 | self.0.as_bytes() 36 | } 37 | } 38 | 39 | impl<'a> AsRef for Footer<'a> { 40 | fn as_ref(&self) -> &str { 41 | self.0 42 | } 43 | } 44 | impl<'a> From<&'a str> for Footer<'a> { 45 | fn from(s: &'a str) -> Self { 46 | Self(s) 47 | } 48 | } 49 | impl<'a> fmt::Display for Footer<'a> { 50 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 51 | write!(f, "{}", self.0) 52 | } 53 | } 54 | impl<'a> PartialEq for Footer<'a> { 55 | fn eq(&self, other: &Self) -> bool { 56 | self.0 == other.0 57 | } 58 | } 59 | impl<'a> Eq for Footer<'a> {} 60 | 61 | #[cfg(test)] 62 | mod unit_tests { 63 | 64 | use super::*; 65 | 66 | #[test] 67 | fn test_v2_footer() { 68 | let footer = Footer::default(); 69 | assert_eq!(footer.as_ref(), ""); 70 | assert!(footer.as_ref().is_empty()); 71 | } 72 | 73 | #[test] 74 | fn test_set_v2_footer() { 75 | let footer: Footer = "wubbulubbadubdub".into(); 76 | assert_eq!(footer.as_ref(), "wubbulubbadubdub"); 77 | assert!(!footer.as_ref().is_empty()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/core/header.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use std::fmt; 3 | use std::fmt::Display; 4 | use std::marker::PhantomData; 5 | use std::ops::Deref; 6 | 7 | /// The [Header] identifies the [protocol version and cryptographic format](https://github.com/paseto-standard/paseto-spec/tree/master/docs/01-Protocol-Versions) for the token 8 | /// 9 | /// [at least one code example that users can copy/paste to try it] 10 | /// 11 | #[derive(PartialEq, Debug, Copy, Clone)] 12 | pub(crate) struct Header 13 | where 14 | Version: VersionTrait, 15 | Purpose: PurposeTrait, 16 | { 17 | version: PhantomData, 18 | purpose: PhantomData, 19 | header: &'static str, 20 | } 21 | 22 | impl Deref for Header { 23 | type Target = [u8]; 24 | 25 | fn deref(&self) -> &Self::Target { 26 | self.header.as_bytes() 27 | } 28 | } 29 | 30 | impl AsRef for Header 31 | where 32 | Version: VersionTrait, 33 | Purpose: PurposeTrait, 34 | { 35 | fn as_ref(&self) -> &str { 36 | self.header 37 | } 38 | } 39 | //note: ugly workaround to minimize heap allocations and allow the full struct to implement Copy 40 | static V1_LOCAL: &str = "v1.local."; 41 | static V1_PUBLIC: &str = "v1.public."; 42 | static V2_LOCAL: &str = "v2.local."; 43 | static V2_PUBLIC: &str = "v2.public."; 44 | static V3_LOCAL: &str = "v3.local."; 45 | static V3_PUBLIC: &str = "v3.public."; 46 | static V4_LOCAL: &str = "v4.local."; 47 | static V4_PUBLIC: &str = "v4.public."; 48 | 49 | impl Default for Header 50 | where 51 | Version: VersionTrait, 52 | Purpose: PurposeTrait, 53 | { 54 | fn default() -> Self { 55 | let header = match (Version::default().as_ref(), Purpose::default().as_ref()) { 56 | ("v1", "local") => V1_LOCAL, 57 | ("v1", "public") => V1_PUBLIC, 58 | ("v2", "local") => V2_LOCAL, 59 | ("v2", "public") => V2_PUBLIC, 60 | ("v3", "local") => V3_LOCAL, 61 | ("v3", "public") => V3_PUBLIC, 62 | ("v4", "local") => V4_LOCAL, 63 | ("v4", "public") => V4_PUBLIC, 64 | _ => "", //this should never happen 65 | }; 66 | Self { 67 | version: PhantomData, 68 | purpose: PhantomData, 69 | header, 70 | } 71 | } 72 | } 73 | 74 | impl Display for Header 75 | where 76 | Version: VersionTrait, 77 | Purpose: PurposeTrait, 78 | { 79 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 80 | write!(f, "{}", self.header) 81 | } 82 | } 83 | 84 | #[cfg(all(test, any(feature = "v4", feature = "v2")))] 85 | mod unit_tests { 86 | 87 | use super::*; 88 | 89 | fn test_header_equality(valid_value: H, header: S) 90 | where 91 | S: AsRef, 92 | H: AsRef, 93 | { 94 | assert_eq!(header.as_ref(), valid_value.as_ref()); 95 | } 96 | 97 | #[cfg(feature = "v4_local")] 98 | #[test] 99 | fn test_v4_local_header_equality() { 100 | test_header_equality(Header::::default(), "v4.local."); 101 | } 102 | 103 | #[cfg(feature = "v4_public")] 104 | #[test] 105 | fn test_v4_public_header_equality() { 106 | test_header_equality(Header::::default(), "v4.public."); 107 | } 108 | 109 | #[cfg(feature = "v2_public")] 110 | #[test] 111 | fn test_v2_public_header_equality() { 112 | test_header_equality(Header::::default(), "v2.public."); 113 | } 114 | 115 | #[cfg(feature = "v2_local")] 116 | #[test] 117 | fn test_v2_local_header_equality() { 118 | test_header_equality(Header::::default(), "v2.local."); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/core/implicit_assertion.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::ops::Deref; 3 | 4 | /// Unencrypted but authenticated data (like the optional footer), but is NOT stored in the PASETO token (thus, implicit) and MUST be asserted when verifying a token. 5 | /// The main purpose for Implicit Assertions is to bind the token to some value that, due to business reasons, shouldn't ever be revealed publicly (i.e., a primary key or foreign key from a relational database table). 6 | /// Implicit Assertions allow you to build systems that are impervious to Confused Deputy attacks without ever having to disclose these internal values. 7 | /// 8 | /// # Usage 9 | /// ``` 10 | /// # #[cfg(feature = "default")] 11 | /// # { 12 | /// # use rusty_paseto::prelude::*; 13 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::from(b"wubbalubbadubdubwubbalubbadubdub")); 14 | /// let token = PasetoBuilder::::default() 15 | /// // note how we set the footer here 16 | /// .set_implicit_assertion(ImplicitAssertion::from("Sometimes science is more art than science")) 17 | /// .build(&key)?; 18 | /// 19 | /// // the footer same footer should be used to parse the token 20 | /// let json_value = PasetoParser::::default() 21 | /// .set_implicit_assertion(ImplicitAssertion::from("Sometimes science is more art than science")) 22 | /// .parse(&token, &key)?; 23 | /// # } 24 | /// # Ok::<(),anyhow::Error>(()) 25 | /// ``` 26 | #[derive(Default, Debug, Copy, Clone)] 27 | pub struct ImplicitAssertion<'a>(&'a str); 28 | 29 | impl<'a> Deref for ImplicitAssertion<'a> { 30 | type Target = [u8]; 31 | 32 | fn deref(&self) -> &'a Self::Target { 33 | self.0.as_bytes() 34 | } 35 | } 36 | 37 | impl<'a> AsRef for ImplicitAssertion<'a> { 38 | fn as_ref(&self) -> &str { 39 | self.0 40 | } 41 | } 42 | impl<'a> From<&'a str> for ImplicitAssertion<'a> { 43 | fn from(s: &'a str) -> Self { 44 | Self(s) 45 | } 46 | } 47 | impl<'a> fmt::Display for ImplicitAssertion<'a> { 48 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 49 | write!(f, "{}", self.0) 50 | } 51 | } 52 | impl<'a> PartialEq for ImplicitAssertion<'a> { 53 | fn eq(&self, other: &Self) -> bool { 54 | self.0 == other.0 55 | } 56 | } 57 | impl<'a> Eq for ImplicitAssertion<'a> {} 58 | -------------------------------------------------------------------------------- /src/core/key/keys.rs: -------------------------------------------------------------------------------- 1 | use crate::core::PasetoError; 2 | use ring::rand::{SecureRandom, SystemRandom}; 3 | use std::convert::{From, TryFrom}; 4 | use std::fmt::Debug; 5 | use std::ops::Deref; 6 | use zeroize::Zeroize; 7 | 8 | /// A wrapper for a slice of bytes that constitute a key of a specific size 9 | #[derive(Zeroize)] 10 | #[zeroize(drop)] 11 | #[derive(Clone)] 12 | pub struct Key([u8; KEYSIZE]); 13 | 14 | impl Default for Key { 15 | fn default() -> Self { 16 | Self([0u8; KEYSIZE]) 17 | } 18 | } 19 | 20 | impl AsRef<[u8]> for Key { 21 | fn as_ref(&self) -> &[u8] { 22 | &self.0 23 | } 24 | } 25 | 26 | impl Deref for Key { 27 | type Target = [u8; KEYSIZE]; 28 | fn deref(&self) -> &Self::Target { 29 | &self.0 30 | } 31 | } 32 | 33 | impl From<&[u8]> for Key { 34 | fn from(key: &[u8]) -> Self { 35 | let mut me = Key::::default(); 36 | me.0.copy_from_slice(key); 37 | me 38 | } 39 | } 40 | 41 | impl From<&[u8; KEYSIZE]> for Key { 42 | fn from(key: &[u8; KEYSIZE]) -> Self { 43 | Self(*key) 44 | } 45 | } 46 | 47 | impl From<[u8; KEYSIZE]> for Key { 48 | fn from(key: [u8; KEYSIZE]) -> Self { 49 | Self(key) 50 | } 51 | } 52 | 53 | impl TryFrom<&str> for Key { 54 | type Error = PasetoError; 55 | fn try_from(value: &str) -> Result { 56 | let key = hex::decode(value).map_err(|_| PasetoError::InvalidKey)?; 57 | if key.len() != KEYSIZE { 58 | return Err(PasetoError::InvalidKey); 59 | } 60 | let mut me = Key::::default(); 61 | me.0.copy_from_slice(&key); 62 | Ok(me) 63 | } 64 | } 65 | 66 | impl Key { 67 | /// Uses the system's RNG to create a random slice of bytes of a specific size 68 | pub fn try_new_random() -> Result { 69 | let rng = SystemRandom::new(); 70 | let mut buf = [0u8; KEYSIZE]; 71 | rng.fill(&mut buf)?; 72 | Ok(Self(buf)) 73 | } 74 | } 75 | 76 | impl Debug for Key { 77 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 78 | write!(f, "!!! KEY IS PRIVATE !!!") 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/core/key/mod.rs: -------------------------------------------------------------------------------- 1 | mod keys; 2 | mod paseto_asymmetric_private_key; 3 | mod paseto_asymmetric_public_key; 4 | mod paseto_nonce; 5 | mod paseto_symmetric_key; 6 | mod paseto_nonce_impl; 7 | 8 | pub use keys::Key; 9 | pub use paseto_asymmetric_private_key::PasetoAsymmetricPrivateKey; 10 | pub use paseto_asymmetric_public_key::PasetoAsymmetricPublicKey; 11 | pub use paseto_nonce::PasetoNonce; 12 | pub use paseto_symmetric_key::PasetoSymmetricKey; 13 | -------------------------------------------------------------------------------- /src/core/key/paseto_asymmetric_private_key.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::core::*; 3 | use std::convert::{AsRef, From}; 4 | use std::marker::PhantomData; 5 | 6 | /// A wrapper for the private half of an asymmetric key pair 7 | /// 8 | /// [V2] and [V4] keys are created from [Key] of size 64, [V1] keys are of an arbitrary size 9 | pub struct PasetoAsymmetricPrivateKey<'a, Version, Purpose> { 10 | version: PhantomData, 11 | purpose: PhantomData, 12 | key: &'a [u8], 13 | } 14 | 15 | impl<'a, Version> From<&'a [u8]> for PasetoAsymmetricPrivateKey<'a, Version, Public> 16 | where 17 | Version: V2orV4, 18 | { 19 | fn from(key: &'a [u8]) -> Self { 20 | Self { 21 | version: PhantomData, 22 | purpose: PhantomData, 23 | key, 24 | } 25 | } 26 | } 27 | 28 | impl<'a, Version, Purpose> AsRef<[u8]> for PasetoAsymmetricPrivateKey<'a, Version, Purpose> { 29 | fn as_ref(&self) -> &[u8] { 30 | self.key 31 | } 32 | } 33 | 34 | #[cfg(feature = "v1_public")] 35 | impl<'a> From<&'a [u8]> for PasetoAsymmetricPrivateKey<'a, V1, Public> { 36 | fn from(key: &'a [u8]) -> Self { 37 | Self { 38 | version: PhantomData, 39 | purpose: PhantomData, 40 | key, 41 | } 42 | } 43 | } 44 | 45 | impl<'a, Version> From<&'a Key<64>> for PasetoAsymmetricPrivateKey<'a, Version, Public> 46 | where 47 | Version: V2orV4, 48 | { 49 | fn from(key: &'a Key<64>) -> Self { 50 | Self { 51 | version: PhantomData, 52 | purpose: PhantomData, 53 | key: key.as_ref(), 54 | } 55 | } 56 | } 57 | 58 | #[cfg(feature = "v3_public")] 59 | impl<'a> From<&'a Key<48>> for PasetoAsymmetricPrivateKey<'a, V3, Public> { 60 | fn from(key: &'a Key<48>) -> Self { 61 | Self { 62 | version: PhantomData, 63 | purpose: PhantomData, 64 | key: key.as_ref(), 65 | } 66 | } 67 | } 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/core/key/paseto_asymmetric_public_key.rs: -------------------------------------------------------------------------------- 1 | use super::Key; 2 | use crate::core::*; 3 | use std::convert::{AsRef, From}; 4 | use std::marker::PhantomData; 5 | /// A wrapper for the public half of an asymmetric key pair 6 | /// 7 | /// [V2] and [V4] keys are created from [Key] of size 32, [V1] keys are of an arbitrary size 8 | pub struct PasetoAsymmetricPublicKey<'a, Version, Purpose> { 9 | version: PhantomData, 10 | purpose: PhantomData, 11 | key: &'a [u8], 12 | } 13 | 14 | impl<'a, Version, Purpose> AsRef<[u8]> for PasetoAsymmetricPublicKey<'a, Version, Purpose> { 15 | fn as_ref(&self) -> &[u8] { 16 | self.key 17 | } 18 | } 19 | 20 | #[cfg(feature = "v1_public")] 21 | impl<'a> From<&'a [u8]> for PasetoAsymmetricPublicKey<'a, V1, Public> { 22 | fn from(key: &'a [u8]) -> Self { 23 | Self { 24 | version: PhantomData, 25 | purpose: PhantomData, 26 | key, 27 | } 28 | } 29 | } 30 | 31 | #[cfg(feature = "v3_public")] 32 | impl<'a> TryFrom<&'a Key<49>> for PasetoAsymmetricPublicKey<'a, V3, Public> { 33 | type Error = PasetoError; 34 | fn try_from(key: &'a Key<49>) -> Result { 35 | if key[0] != 2 && key[0] != 3 { 36 | return Err(PasetoError::InvalidKey); 37 | } 38 | //if this is successful, we can be sure our key is in a valid format 39 | Ok(Self { 40 | version: PhantomData, 41 | purpose: PhantomData, 42 | key: key.as_ref(), 43 | }) 44 | } 45 | } 46 | 47 | impl<'a, Version> From<&'a Key<32>> for PasetoAsymmetricPublicKey<'a, Version, Public> 48 | where 49 | Version: V2orV4, 50 | { 51 | fn from(key: &'a Key<32>) -> Self { 52 | Self { 53 | version: PhantomData, 54 | purpose: PhantomData, 55 | key: key.as_ref(), 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/core/key/paseto_nonce.rs: -------------------------------------------------------------------------------- 1 | use std::convert::AsRef; 2 | use std::marker::PhantomData; 3 | use std::ops::Deref; 4 | 5 | /// A nonce key for use in PASETO algorithms 6 | /// 7 | /// Key sizes for nonces are either 32 or 24 bytes in size 8 | /// 9 | /// Nonces can be specified directly for testing or randomly in production 10 | /// # Example usage 11 | /// ``` 12 | /// # #[cfg(feature = "v4_local")] 13 | /// # { 14 | /// use serde_json::json; 15 | /// use rusty_paseto::core::*; 16 | /// 17 | /// let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 18 | /// // generate a random nonce with 19 | /// let nonce = Key::<32>::try_new_random()?; 20 | /// let nonce = PasetoNonce::::from(&nonce); 21 | /// 22 | /// let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 23 | /// let payload = payload.as_str(); 24 | /// let payload = Payload::from(payload); 25 | /// 26 | /// //create a public v4 token 27 | /// let token = Paseto::::builder() 28 | /// .set_payload(payload) 29 | /// .try_encrypt(&key, &nonce)?; 30 | /// # } 31 | /// # Ok::<(),anyhow::Error>(()) 32 | /// ``` 33 | 34 | 35 | pub struct PasetoNonce<'a, Version, Purpose> { 36 | pub(crate) version: PhantomData, 37 | pub(crate) purpose: PhantomData, 38 | pub(crate) key: &'a [u8], 39 | } 40 | 41 | impl<'a, Version, Purpose> Deref for PasetoNonce<'a, Version, Purpose> { 42 | type Target = [u8]; 43 | fn deref(&self) -> &Self::Target { 44 | self.key 45 | } 46 | } 47 | 48 | impl<'a, Version, Purpose> AsRef<[u8]> for PasetoNonce<'a, Version, Purpose> { 49 | fn as_ref(&self) -> &[u8] { 50 | self.key 51 | } 52 | } 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/core/key/paseto_nonce_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod v1_local; 2 | mod v2_local; 3 | mod v3_local; 4 | mod v4_local; 5 | mod v2_public; -------------------------------------------------------------------------------- /src/core/key/paseto_nonce_impl/v1_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v1_local")] 2 | use std::marker::PhantomData; 3 | use crate::core::{Key, Local, PasetoNonce, V1}; 4 | 5 | impl<'a> From<&'a Key<32>> for PasetoNonce<'a, V1, Local> { 6 | fn from(key: &'a Key<32>) -> Self { 7 | Self { 8 | version: PhantomData, 9 | purpose: PhantomData, 10 | key: key.as_ref(), 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/core/key/paseto_nonce_impl/v2_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v2_local")] 2 | use std::marker::PhantomData; 3 | use crate::core::{Key, Local, PasetoNonce, V2}; 4 | 5 | impl<'a> From<&'a Key<24>> for PasetoNonce<'a, V2, Local> { 6 | fn from(key: &'a Key<24>) -> Self { 7 | Self { 8 | version: PhantomData, 9 | purpose: PhantomData, 10 | key: key.as_ref(), 11 | } 12 | } 13 | } 14 | 15 | impl<'a> From<&'a Key<32>> for PasetoNonce<'a, V2, Local> { 16 | fn from(key: &'a Key<32>) -> Self { 17 | Self { 18 | version: PhantomData, 19 | purpose: PhantomData, 20 | key: key.as_ref(), 21 | } 22 | } 23 | } 24 | 25 | #[cfg(all(test, feature = "v2_local"))] 26 | mod builders { 27 | use std::convert::From; 28 | 29 | use crate::core::*; 30 | use anyhow::Result; 31 | 32 | use super::PasetoNonce; 33 | 34 | #[test] 35 | fn v2_local_key_test() -> Result<()> { 36 | let key = Key::<32>::from(b"wubbalubbadubdubwubbalubbadubdub"); 37 | let paseto_key = PasetoNonce::::from(&key); 38 | assert_eq!(paseto_key.as_ref().len(), key.as_ref().len()); 39 | Ok(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/core/key/paseto_nonce_impl/v2_public.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v2_public")] 2 | use std::marker::PhantomData; 3 | use crate::core::{PasetoNonce, Public, V2}; 4 | 5 | impl<'a, T> From<&'a T> for PasetoNonce<'a, V2, Public> 6 | where 7 | T: Into<&'a [u8]>, 8 | &'a [u8]: From<&'a T>, 9 | { 10 | fn from(key: &'a T) -> Self { 11 | Self { 12 | version: PhantomData, 13 | purpose: PhantomData, 14 | key: key.into(), 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/key/paseto_nonce_impl/v3_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v3_local")] 2 | use std::marker::PhantomData; 3 | use crate::core::{Key, Local, PasetoNonce, V3}; 4 | 5 | impl<'a> From<&'a Key<32>> for PasetoNonce<'a, V3, Local> { 6 | fn from(key: &'a Key<32>) -> Self { 7 | Self { 8 | version: PhantomData, 9 | purpose: PhantomData, 10 | key: key.as_ref(), 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/core/key/paseto_nonce_impl/v4_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v4_local")] 2 | use std::marker::PhantomData; 3 | use crate::core::{Key, Local, PasetoNonce, V4}; 4 | 5 | impl<'a> From<&'a Key<32>> for PasetoNonce<'a, V4, Local> { 6 | fn from(key: &'a Key<32>) -> Self { 7 | Self { 8 | version: PhantomData, 9 | purpose: PhantomData, 10 | key: key.as_ref(), 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/core/key/paseto_symmetric_key.rs: -------------------------------------------------------------------------------- 1 | use super::Key; 2 | use crate::core::Local; 3 | use std::convert::{AsRef, From}; 4 | use std::marker::PhantomData; 5 | 6 | /// A wrapper for a symmetric key 7 | /// 8 | /// Keys are created from [Key] of size 32 9 | pub struct PasetoSymmetricKey { 10 | version: PhantomData, 11 | purpose: PhantomData, 12 | key: Key<32>, 13 | } 14 | 15 | impl From> for PasetoSymmetricKey { 16 | fn from(key: Key<32>) -> Self { 17 | Self { 18 | version: PhantomData, 19 | purpose: PhantomData, 20 | key, 21 | } 22 | } 23 | } 24 | 25 | impl AsRef<[u8]> for PasetoSymmetricKey { 26 | fn as_ref(&self) -> &[u8] { 27 | self.key.as_ref() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/core/mod.rs: -------------------------------------------------------------------------------- 1 | //! The **core** architectural layer and feature contains only paseto primitives for lightweight 2 | //! encrypting / decrypting or signing / verification 3 | //! 4 | //! ![paseto_core_small](https://user-images.githubusercontent.com/24578097/147881920-14c52256-1a0c-40be-9f18-759a8c9ad77d.png) 5 | //! 6 | //! The **core** feature requires you to specify the version and purpose 7 | //! ```toml 8 | //! ## Includes only v4 modern sodium cipher crypto core and local (symmetric) 9 | //! ## key types with NO claims, defaults or validation, just basic PASETO 10 | //! ## encrypt/signing and decrypt/verification. 11 | //! 12 | //! rusty_paseto = {version = "latest", features = ["core", "v4_local"] } 13 | //! 14 | //! ``` 15 | //! # Example usage 16 | //! ``` 17 | //! # #[cfg(feature = "v4_local")] 18 | //! # { 19 | //! # use serde_json::json; 20 | //! use rusty_paseto::core::*; 21 | //! 22 | //! let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 23 | //! let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 24 | //! // generate a random nonce with 25 | //! // let nonce = Key::<32>::try_new_random()?; 26 | //! let nonce = PasetoNonce::::from(&nonce); 27 | //! 28 | //! let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 29 | //! let payload = payload.as_str(); 30 | //! let payload = Payload::from(payload); 31 | //! 32 | //! //create a public v4 token 33 | //! let token = Paseto::::builder() 34 | //! .set_payload(payload) 35 | //! .try_encrypt(&key, &nonce)?; 36 | //! 37 | //! //validate the test vector 38 | //! assert_eq!(token.to_string(), "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvSwscFlAl1pk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XJ5hOb_4v9RmDkneN0S92dx0OW4pgy7omxgf3S8c3LlQg"); 39 | //! 40 | //! //now let's try to decrypt it 41 | //! let json = Paseto::::try_decrypt(&token, &key, None, None)?; 42 | //! assert_eq!(payload, json); 43 | //! } 44 | //! # Ok::<(),anyhow::Error>(()) 45 | //! ``` 46 | 47 | mod error; 48 | mod footer; 49 | mod header; 50 | mod implicit_assertion; 51 | mod key; 52 | mod paseto; 53 | mod payload; 54 | mod purpose; 55 | mod traits; 56 | mod version; 57 | mod common; 58 | mod paseto_impl; 59 | 60 | pub use error::PasetoError; 61 | pub use footer::Footer; 62 | pub(crate) use header::Header; 63 | pub use implicit_assertion::ImplicitAssertion; 64 | pub use key::{Key, PasetoAsymmetricPrivateKey, PasetoAsymmetricPublicKey, PasetoNonce, PasetoSymmetricKey}; 65 | pub use paseto::Paseto; 66 | pub use payload::Payload; 67 | pub use purpose::{Local, Public}; 68 | pub(crate) use traits::{Base64Encodable, V1orV3, V2orV4}; 69 | pub use traits::{ImplicitAssertionCapable, PurposeTrait, VersionTrait}; 70 | pub use version::*; 71 | -------------------------------------------------------------------------------- /src/core/paseto.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | str, 3 | }; 4 | use crate::core::{Base64Encodable, Footer, Header, ImplicitAssertion, ImplicitAssertionCapable, PasetoError, Payload, PurposeTrait, VersionTrait}; 5 | 6 | 7 | /// Used to build and encrypt / decrypt core PASETO tokens 8 | 9 | /// 10 | /// Given a [Payload], optional [Footer] and optional [ImplicitAssertion] ([V3] or [V4] only) 11 | /// returns an encrypted token when [Local] is specified as the purpose or a signed token when 12 | /// [Public] is specified 13 | /// # Example usage 14 | /// ``` 15 | /// # #[cfg(feature = "v4_local")] 16 | /// # { 17 | /// # use serde_json::json; 18 | /// use rusty_paseto::core::*; 19 | /// 20 | /// let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 21 | /// let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 22 | /// // generate a random nonce with 23 | /// // let nonce = Key::<32>::try_new_random()?; 24 | /// let nonce = PasetoNonce::::from(&nonce); 25 | /// 26 | /// let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 27 | /// let payload = payload.as_str(); 28 | /// let payload = Payload::from(payload); 29 | /// 30 | /// //create a public v4 token 31 | /// let token = Paseto::::builder() 32 | /// .set_payload(payload) 33 | /// .try_encrypt(&key, &nonce)?; 34 | /// 35 | /// //validate the test vector 36 | /// assert_eq!(token.to_string(), "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvSwscFlAl1pk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XJ5hOb_4v9RmDkneN0S92dx0OW4pgy7omxgf3S8c3LlQg"); 37 | /// 38 | /// //now let's try to decrypt it 39 | /// let json = Paseto::::try_decrypt(&token, &key, None, None)?; 40 | /// assert_eq!(payload, json); 41 | /// } 42 | /// # Ok::<(),anyhow::Error>(()) 43 | /// ``` 44 | #[derive(Default, Copy, Clone)] 45 | pub struct Paseto<'a, Version, Purpose> 46 | where 47 | Version: VersionTrait, 48 | Purpose: PurposeTrait, 49 | { 50 | pub(crate) header: Header, 51 | pub(crate) payload: Payload<'a>, 52 | pub(crate) footer: Option>, 53 | pub(crate) implicit_assertion: Option>, 54 | } 55 | 56 | impl<'a, Version: VersionTrait, Purpose: PurposeTrait> Paseto<'a, Version, Purpose> { 57 | /// Returns a builder for creating a PASETO token 58 | /// 59 | /// # Example usage 60 | /// ``` 61 | /// # #[cfg(feature = "v4_local")] 62 | /// # { 63 | /// # use serde_json::json; 64 | /// # use rusty_paseto::core::*; 65 | /// 66 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 67 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 68 | /// # // generate a random nonce with 69 | /// # // let nonce = Key::<32>::try_new_random()?; 70 | /// # let nonce = PasetoNonce::::from(&nonce); 71 | /// 72 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 73 | /// # let payload = payload.as_str(); 74 | /// # let payload = Payload::from(payload); 75 | /// 76 | /// //create a public v4 token 77 | /// let token = Paseto::::builder() 78 | /// .set_payload(payload) 79 | /// .try_encrypt(&key, &nonce)?; 80 | /// 81 | /// # //validate the test vector 82 | /// # assert_eq!(token.to_string(), "v4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAr68PS4AXe7If_ZgesdkUMvSwscFlAl1pk5HC0e8kApeaqMfGo_7OpBnwJOAbY9V7WU6abu74MmcUE8YWAiaArVI8XJ5hOb_4v9RmDkneN0S92dx0OW4pgy7omxgf3S8c3LlQg"); 83 | /// 84 | /// # //now let's try to decrypt it 85 | /// # let json = Paseto::::try_decrypt(&token, &key, None, None)?; 86 | /// # assert_eq!(payload, json); 87 | /// } 88 | /// # Ok::<(),anyhow::Error>(()) 89 | /// ``` 90 | pub fn builder() -> Paseto<'a, Version, Purpose> { 91 | Self { ..Default::default() } 92 | } 93 | 94 | /// Sets the payload for the token 95 | pub fn set_payload(&mut self, payload: Payload<'a>) -> &mut Self { 96 | self.payload = payload; 97 | self 98 | } 99 | 100 | /// Sets an optional footer for the token 101 | /// 102 | /// # Example usage 103 | /// ``` 104 | /// # #[cfg(feature = "v4_local")] 105 | /// # { 106 | /// # use serde_json::json; 107 | /// # use rusty_paseto::core::*; 108 | /// 109 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 110 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 111 | /// # // generate a random nonce with 112 | /// # // let nonce = Key::<32>::try_new_random()?; 113 | /// # let nonce = PasetoNonce::::from(&nonce); 114 | /// 115 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 116 | /// # let payload = payload.as_str(); 117 | /// # let payload = Payload::from(payload); 118 | /// 119 | /// // Set the footer with a Footer struct 120 | /// let token = Paseto::::builder() 121 | /// .set_payload(payload) 122 | /// .set_footer(Footer::from("Supah doopah!")) 123 | /// .try_encrypt(&key, &nonce)?; 124 | /// 125 | /// # //now let's try to decrypt it 126 | /// # let json = Paseto::::try_decrypt(&token, &key, Footer::from("Supah doopah!"), None)?; 127 | /// # assert_eq!(payload, json); 128 | /// } 129 | /// # Ok::<(),anyhow::Error>(()) 130 | /// ``` 131 | pub fn set_footer(&mut self, footer: Footer<'a>) -> &mut Self { 132 | self.footer = Some(footer); 133 | self 134 | } 135 | 136 | /* BEGIN PRIVATE FUNCTIONS */ 137 | pub(crate) fn format_token(&self, encrypted_payload: &str) -> String { 138 | let footer = self.footer.map(|f| f.encode()); 139 | match footer { 140 | Some(f) => format!("{}{}.{}", self.header, encrypted_payload, f), 141 | None => format!("{}{}", self.header, encrypted_payload), 142 | } 143 | } 144 | 145 | pub(crate) fn parse_raw_token( 146 | raw_token: &'a str, 147 | footer: (impl Into>> + Copy), 148 | v: &Version, 149 | p: &Purpose, 150 | ) -> Result, PasetoError> { 151 | //split the raw token into parts 152 | let potential_parts = raw_token.split('.').collect::>(); 153 | //inspect the parts 154 | match potential_parts.len() { 155 | length if !(3..=4).contains(&length) => { 156 | return Err(PasetoError::IncorrectSize); 157 | } 158 | 4 => { 159 | //verify expected footer 160 | let footer = footer.into().unwrap_or_default(); 161 | let found_footer = Footer::from(potential_parts[3]); 162 | if !footer.constant_time_equals(found_footer) { 163 | return Err(PasetoError::FooterInvalid); 164 | } 165 | } 166 | _ => {} 167 | } 168 | 169 | //grab the header 170 | let potential_header = format!("{}.{}.", potential_parts[0], potential_parts[1]); 171 | //we should be able to verify the header using the passed in Version and Purpose 172 | let expected_header = format!("{}.{}.", v, p); 173 | 174 | //verify the header 175 | if potential_header.ne(&expected_header) { 176 | return Err(PasetoError::WrongHeader); 177 | }; 178 | 179 | let encrypted_payload = Payload::from(potential_parts[2]); 180 | Ok(encrypted_payload.decode()?) 181 | } 182 | /* END PRIVATE FUNCTIONS */ 183 | } 184 | 185 | impl<'a, Version, Purpose> Paseto<'a, Version, Purpose> 186 | where 187 | Purpose: PurposeTrait, 188 | Version: ImplicitAssertionCapable, 189 | { 190 | /// Sets an optional [ImplicitAssertion] for the token 191 | /// 192 | /// *NOTE:* Only for [V3] or [V4] tokens 193 | /// 194 | /// # Example usage 195 | /// ``` 196 | /// # #[cfg(feature = "v4_local")] 197 | /// # { 198 | /// # use serde_json::json; 199 | /// # use rusty_paseto::core::*; 200 | /// 201 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 202 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 203 | /// # // generate a random nonce with 204 | /// # // let nonce = Key::<32>::try_new_random()?; 205 | /// # let nonce = PasetoNonce::::from(&nonce); 206 | /// 207 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 208 | /// # let payload = payload.as_str(); 209 | /// # let payload = Payload::from(payload); 210 | /// 211 | /// // Set the ImplicitAssertion 212 | /// let token = Paseto::::builder() 213 | /// .set_payload(payload) 214 | /// .set_implicit_assertion(ImplicitAssertion::from("Supah doopah!")) 215 | /// .try_encrypt(&key, &nonce)?; 216 | /// 217 | /// # //now let's try to decrypt it 218 | /// # let json = Paseto::::try_decrypt(&token, &key, None, ImplicitAssertion::from("Supah doopah!"))?; 219 | /// # assert_eq!(payload, json); 220 | /// } 221 | /// # Ok::<(),anyhow::Error>(()) 222 | /// ``` 223 | pub fn set_implicit_assertion(&mut self, implicit_assertion: ImplicitAssertion<'a>) -> &mut Self { 224 | self.implicit_assertion = Some(implicit_assertion); 225 | self 226 | } 227 | } 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /src/core/paseto_impl/mod.rs: -------------------------------------------------------------------------------- 1 | mod v1_public; 2 | mod v2_public; 3 | mod v1_local; 4 | mod v2_local; 5 | mod v3_local; 6 | mod v4_local; 7 | mod v4_public; 8 | mod v3_public; -------------------------------------------------------------------------------- /src/core/paseto_impl/v1_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v1_local")] 2 | use std::str; 3 | use hmac::{Hmac, Mac}; 4 | use crate::core::{Footer, Header, Key, Local, Paseto, PasetoError, PasetoNonce, PasetoSymmetricKey, V1}; 5 | use crate::core::common::{AuthenticationKey, AuthenticationKeySeparator, CipherText, EncryptionKey, EncryptionKeySeparator, PreAuthenticationEncoding, RawPayload, Tag}; 6 | use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals; 7 | use sha2::Sha384; 8 | 9 | impl<'a> Paseto<'a, V1, Local> { 10 | /// Attempts to decrypt a PASETO token 11 | /// ``` 12 | /// # use serde_json::json; 13 | /// # use rusty_paseto::core::*; 14 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 15 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 16 | /// # let nonce = PasetoNonce::::from(&nonce); 17 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 18 | /// # let payload = payload.as_str(); 19 | /// # let payload = Payload::from(payload); 20 | /// # let token = Paseto::::builder().set_payload(payload).try_encrypt(&key, &nonce)?; 21 | /// // decrypt a public v1 token 22 | /// let json = Paseto::::try_decrypt(&token, &key, None)?; 23 | /// # assert_eq!(payload, json); 24 | /// # Ok::<(),anyhow::Error>(()) 25 | /// ``` 26 | pub fn try_decrypt( 27 | token: &'a str, 28 | key: &PasetoSymmetricKey, 29 | footer: (impl Into>> + Copy), 30 | ) -> Result { 31 | let decoded_payload = Self::parse_raw_token(token, footer, &V1::default(), &Local::default())?; 32 | let nonce = Key::from(&decoded_payload[..32]); 33 | let nonce = PasetoNonce::::from(&nonce); 34 | 35 | let aks: &[u8] = &AuthenticationKeySeparator::default(); 36 | let authentication_key = AuthenticationKey::::try_from(&Key::from(aks), key, &nonce)?; 37 | let eks: &[u8] = &EncryptionKeySeparator::default(); 38 | let encryption_key = EncryptionKey::::try_from(&Key::from(eks), key, &nonce)?; 39 | 40 | let ciphertext = &decoded_payload[32..(decoded_payload.len() - 48)]; 41 | 42 | //pack preauth 43 | let pae = PreAuthenticationEncoding::parse(&[ 44 | &Header::::default(), 45 | nonce.as_ref(), 46 | ciphertext, 47 | &footer.into().unwrap_or_default(), 48 | ]); 49 | 50 | //generate tags 51 | let tag = &decoded_payload[(nonce.len() + ciphertext.len())..]; 52 | let tag2 = &Tag::::from(authentication_key, &pae); 53 | //compare tags 54 | ConstantTimeEquals(tag, tag2)?; 55 | 56 | //decrypt payload 57 | let ciphertext = CipherText::::from(ciphertext, &encryption_key); 58 | 59 | let decoded_str = str::from_utf8(&ciphertext)?; 60 | 61 | //return decrypted payload 62 | Ok(decoded_str.to_owned()) 63 | } 64 | 65 | /// Attempts to encrypt a PASETO token 66 | /// ``` 67 | /// # use serde_json::json; 68 | /// # use rusty_paseto::core::*; 69 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 70 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 71 | /// # let nonce = PasetoNonce::::from(&nonce); 72 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 73 | /// # let payload = payload.as_str(); 74 | /// # let payload = Payload::from(payload); 75 | /// // encrypt a public v1 token 76 | /// let token = Paseto::::builder().set_payload(payload).try_encrypt(&key, &nonce)?; 77 | /// # let json = Paseto::::try_decrypt(&token, &key, None)?; 78 | /// # assert_eq!(payload, json); 79 | /// # Ok::<(),anyhow::Error>(()) 80 | /// ``` 81 | pub fn try_encrypt( 82 | &mut self, 83 | key: &PasetoSymmetricKey, 84 | nonce: &PasetoNonce, 85 | ) -> Result { 86 | //setup 87 | let footer = self.footer.unwrap_or_default(); 88 | 89 | //calculate nonce 90 | type HmacSha384 = Hmac; 91 | let mut mac = HmacSha384::new_from_slice(nonce.as_ref()).expect("HMAC can take key of any size"); 92 | 93 | mac.update(&self.payload); 94 | let out = mac.finalize(); 95 | let nonce = Key::from(&out.into_bytes()[..32]); 96 | let nonce = PasetoNonce::::from(&nonce); 97 | 98 | //split key 99 | let aks: &[u8] = &AuthenticationKeySeparator::default(); 100 | let authentication_key = AuthenticationKey::::try_from(&Key::from(aks), key, &nonce)?; 101 | let eks: &[u8] = &EncryptionKeySeparator::default(); 102 | let encryption_key = EncryptionKey::::try_from(&Key::from(eks), key, &nonce)?; 103 | 104 | //encrypt payload 105 | let ciphertext = CipherText::::from(&self.payload, &encryption_key); 106 | 107 | //pack preauth 108 | let pae = PreAuthenticationEncoding::parse(&[&self.header, nonce.as_ref(), &ciphertext, &footer]); 109 | 110 | // //generate tag 111 | let tag = Tag::::from(authentication_key, &pae); 112 | 113 | // //generate appended and base64 encoded payload 114 | let raw_payload = RawPayload::::from(&nonce, &ciphertext, &tag)?; 115 | 116 | //format as paseto with header and optional footer 117 | Ok(self.format_token(&raw_payload)) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/core/paseto_impl/v1_public.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v1_public")] 2 | use ring::rand::SystemRandom; 3 | use ring::signature::{RSA_PSS_SHA384, RsaKeyPair}; 4 | use crate::core::{Footer, Paseto, PasetoAsymmetricPrivateKey, PasetoAsymmetricPublicKey, PasetoError, Public, V1}; 5 | use crate::core::common::{CipherText, PreAuthenticationEncoding, RawPayload}; 6 | 7 | impl<'a> Paseto<'a, V1, Public> { 8 | /// Verifies a signed V1 Public Paseto 9 | pub fn try_verify( 10 | signature: &'a str, 11 | public_key: &PasetoAsymmetricPublicKey, 12 | footer: (impl Into>> + Copy), 13 | ) -> Result { 14 | let decoded_payload = Self::parse_raw_token(signature, footer, &V1::default(), &Public::default())?; 15 | 16 | let ciphertext = 17 | CipherText::::try_verify(&decoded_payload, public_key, &footer.into().unwrap_or_default())? 18 | .ciphertext; 19 | 20 | Ok(String::from_utf8(ciphertext)?) 21 | } 22 | 23 | /// Attempts to sign a V1 Public Paseto 24 | /// Fails with a PasetoError if the token is malformed or the private key isn't in a valid pkcs#8 25 | /// format 26 | pub fn try_sign(&mut self, key: &PasetoAsymmetricPrivateKey) -> Result { 27 | let footer = self.footer.unwrap_or_default(); 28 | 29 | let key_pair = RsaKeyPair::from_pkcs8(key.as_ref())?; 30 | 31 | let pae = PreAuthenticationEncoding::parse(&[&self.header, &self.payload, &footer]); 32 | let random = SystemRandom::new(); 33 | 34 | let mut signature = [0; 256]; 35 | 36 | key_pair 37 | .sign(&RSA_PSS_SHA384, &random, &pae, &mut signature) 38 | .map_err(|_| PasetoError::InvalidSignature)?; 39 | 40 | let raw_payload = RawPayload::::from(&self.payload, &signature); 41 | 42 | Ok(self.format_token(&raw_payload)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/core/paseto_impl/v2_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v2_local")] 2 | use blake2::Blake2bMac; 3 | use blake2::digest::{FixedOutput, Mac}; 4 | use chacha20poly1305::XNonce; 5 | use crate::core::{Footer, Header, Local, Paseto, PasetoError, PasetoNonce, PasetoSymmetricKey, V2}; 6 | use crate::core::common::{CipherText, PreAuthenticationEncoding, RawPayload}; 7 | use std::str; 8 | impl<'a> Paseto<'a, V2, Local> { 9 | /// Attempts to decrypt a PASETO token 10 | /// ``` 11 | /// # use serde_json::json; 12 | /// # use rusty_paseto::core::*; 13 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 14 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 15 | /// # let nonce = PasetoNonce::::from(&nonce); 16 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 17 | /// # let payload = payload.as_str(); 18 | /// # let payload = Payload::from(payload); 19 | /// # let token = Paseto::::builder().set_payload(payload).try_encrypt(&key, &nonce)?; 20 | /// // decrypt a public v2 token 21 | /// let json = Paseto::::try_decrypt(&token, &key, None)?; 22 | /// # assert_eq!(payload, json); 23 | /// # Ok::<(),anyhow::Error>(()) 24 | /// ``` 25 | pub fn try_decrypt( 26 | token: &'a str, 27 | key: &PasetoSymmetricKey, 28 | footer: (impl Into>> + Copy), 29 | ) -> Result { 30 | //get footer 31 | 32 | let decoded_payload = Self::parse_raw_token(token, footer, &V2::default(), &Local::default())?; 33 | let (nonce, ciphertext) = decoded_payload.split_at(24); 34 | 35 | //pack preauth 36 | let pae = &PreAuthenticationEncoding::parse(&[ 37 | &Header::::default(), 38 | nonce, 39 | &footer.into().unwrap_or_default(), 40 | ]); 41 | 42 | //create the nonce 43 | let nonce = XNonce::from_slice(nonce); 44 | 45 | //encrypt payload 46 | let ciphertext = CipherText::::try_decrypt_from(key, nonce, ciphertext, pae)?; 47 | 48 | //generate appended and base64 encoded payload 49 | let decoded_str = str::from_utf8(&ciphertext)?; 50 | 51 | //return decrypted payload 52 | Ok(decoded_str.to_owned()) 53 | } 54 | 55 | /// Attempts to encrypt a PASETO token 56 | /// ``` 57 | /// # use serde_json::json; 58 | /// # use rusty_paseto::core::*; 59 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 60 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 61 | /// # let nonce = PasetoNonce::::from(&nonce); 62 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 63 | /// # let payload = payload.as_str(); 64 | /// # let payload = Payload::from(payload); 65 | /// // encrypt a public v2 token 66 | /// let token = Paseto::::builder().set_payload(payload).try_encrypt(&key, &nonce)?; 67 | /// # let json = Paseto::::try_decrypt(&token, &key, None)?; 68 | /// # assert_eq!(payload, json); 69 | /// # Ok::<(),anyhow::Error>(()) 70 | /// ``` 71 | pub fn try_encrypt( 72 | &self, 73 | key: &PasetoSymmetricKey, 74 | nonce: &PasetoNonce, 75 | ) -> Result { 76 | //setup 77 | let footer = self.footer.unwrap_or_default(); 78 | 79 | //create the blake2 context to generate the nonce 80 | let mut blake2 = Blake2bMac::new_from_slice(nonce.as_ref())?; 81 | blake2.update(&self.payload); 82 | let mut context = [0u8; 24]; 83 | blake2.finalize_into((&mut context).into()); 84 | 85 | //create the nonce 86 | let nonce = XNonce::from_slice(&context); 87 | 88 | //pack preauth 89 | let pae = PreAuthenticationEncoding::parse(&[&self.header, nonce, &footer]); 90 | 91 | //encrypt payload 92 | let ciphertext = CipherText::::try_from(key, nonce, &self.payload, &pae)?; 93 | 94 | //generate appended and base64 encoded payload 95 | let raw_payload = RawPayload::::from(&context, &ciphertext); 96 | 97 | //format as paseto with header and optional footer 98 | Ok(self.format_token(&raw_payload)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/core/paseto_impl/v2_public.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v2_public")] 2 | use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; 3 | use crate::core::{Footer, Header, Paseto, PasetoAsymmetricPrivateKey, PasetoAsymmetricPublicKey, PasetoError, Public, V2}; 4 | use crate::core::common::{PreAuthenticationEncoding, RawPayload}; 5 | 6 | impl<'a> Paseto<'a, V2, Public> { 7 | /// Attempts to verify a signed V2 Public Paseto 8 | /// Fails with a PasetoError if the token is malformed or the token cannot be verified with the 9 | /// passed public key 10 | pub fn try_verify( 11 | signature: &'a str, 12 | public_key: &PasetoAsymmetricPublicKey, 13 | footer: (impl Into>> + Copy), 14 | ) -> Result { 15 | let decoded_payload = Self::parse_raw_token(signature, footer, &V2::default(), &Public::default())?; 16 | 17 | let verifying_key: VerifyingKey = VerifyingKey::from_bytes(<&[u8; 32]>::try_from(public_key.as_ref())?)?; 18 | 19 | // let public_key = PublicKey::from_bytes(public_key.as_ref()).map_err(|_| PasetoError::InvalidSignature)?; 20 | let msg = decoded_payload[..(decoded_payload.len() - ed25519_dalek::SIGNATURE_LENGTH)].as_ref(); 21 | let sig = decoded_payload[msg.len()..msg.len() + ed25519_dalek::SIGNATURE_LENGTH].as_ref(); 22 | 23 | let signature = Signature::try_from(sig).map_err(|_| PasetoError::InvalidSignature)?; 24 | let pae = PreAuthenticationEncoding::parse(&[ 25 | &Header::::default(), 26 | msg, 27 | &footer.into().unwrap_or_default(), 28 | ]); 29 | 30 | verifying_key.verify(&pae, &signature)?; 31 | // public_key 32 | // .verify(&pae, &signature) 33 | // .map_err(|_| PasetoError::InvalidSignature)?; 34 | 35 | Ok(String::from_utf8(Vec::from(msg))?) 36 | } 37 | 38 | /// Attempts to sign a V2 Public Paseto 39 | /// Fails with a PasetoError if the token is malformed or the private key can't be parsed 40 | pub fn try_sign(&mut self, key: &PasetoAsymmetricPrivateKey) -> Result { 41 | let footer = self.footer.unwrap_or_default(); 42 | 43 | // let keypair = Keypair::from_bytes(key.as_ref())?; 44 | let signing_key = SigningKey::from_keypair_bytes(<&[u8; 64]>::try_from(key.as_ref())?)?; 45 | 46 | let pae = PreAuthenticationEncoding::parse(&[&self.header, &self.payload, &footer]); 47 | 48 | // let signature = keypair.sign(&pae); 49 | let signature = signing_key.sign(&pae); 50 | let raw_payload = RawPayload::::from(&self.payload, &signature.to_bytes()); 51 | 52 | Ok(self.format_token(&raw_payload)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/core/paseto_impl/v3_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v3_local")] 2 | 3 | use std::str; 4 | 5 | use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals; 6 | 7 | use crate::core::{Footer, Header, ImplicitAssertion, Key, Local, Paseto, PasetoError, PasetoNonce, PasetoSymmetricKey, V3}; 8 | use crate::core::common::{AuthenticationKey, AuthenticationKeySeparator, CipherText, EncryptionKey, EncryptionKeySeparator, PreAuthenticationEncoding, RawPayload, Tag}; 9 | 10 | impl<'a> Paseto<'a, V3, Local> { 11 | /// Attempts to decrypt a PASETO token 12 | /// ``` 13 | /// # use serde_json::json; 14 | /// # use rusty_paseto::core::*; 15 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 16 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 17 | /// # let nonce = PasetoNonce::::from(&nonce); 18 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 19 | /// # let payload = payload.as_str(); 20 | /// # let payload = Payload::from(payload); 21 | /// # let token = Paseto::::builder().set_payload(payload).try_encrypt(&key, &nonce)?; 22 | /// // decrypt a public v3 token 23 | /// let json = Paseto::::try_decrypt(&token, &key, None, None)?; 24 | /// # assert_eq!(payload, json); 25 | /// # Ok::<(),anyhow::Error>(()) 26 | /// ``` 27 | pub fn try_decrypt( 28 | token: &'a str, 29 | key: &PasetoSymmetricKey, 30 | footer: (impl Into>> + Copy), 31 | implicit_assertion: (impl Into>> + Copy), 32 | ) -> Result { 33 | //get footer 34 | 35 | let decoded_payload = Self::parse_raw_token(token, footer, &V3::default(), &Local::default())?; 36 | let nonce = Key::from(&decoded_payload[..32]); 37 | let nonce = PasetoNonce::::from(&nonce); 38 | 39 | let authentication_key = 40 | AuthenticationKey::::try_from(&(AuthenticationKeySeparator::default() + &nonce), key)?; 41 | let encryption_key = EncryptionKey::::try_from(&(EncryptionKeySeparator::default() + &nonce), key)?; 42 | 43 | let ciphertext = &decoded_payload[32..(decoded_payload.len() - 48)]; 44 | 45 | //pack preauth 46 | let pae = PreAuthenticationEncoding::parse(&[ 47 | &Header::::default(), 48 | nonce.as_ref(), 49 | ciphertext, 50 | &footer.into().unwrap_or_default(), 51 | &implicit_assertion.into().unwrap_or_default(), 52 | ]); 53 | 54 | //generate tags 55 | let tag = &decoded_payload[(nonce.len() + ciphertext.len())..]; 56 | let tag2 = &Tag::::from(authentication_key, &pae); 57 | //compare tags 58 | ConstantTimeEquals(tag, tag2)?; 59 | 60 | //decrypt payload 61 | let ciphertext = CipherText::::from(ciphertext, &encryption_key); 62 | 63 | let decoded_str = str::from_utf8(&ciphertext)?; 64 | 65 | //return decrypted payload 66 | Ok(decoded_str.to_owned()) 67 | } 68 | 69 | /// Attempts to encrypt a PASETO token 70 | /// ``` 71 | /// # use serde_json::json; 72 | /// # use rusty_paseto::core::*; 73 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 74 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 75 | /// # let nonce = PasetoNonce::::from(&nonce); 76 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 77 | /// # let payload = payload.as_str(); 78 | /// # let payload = Payload::from(payload); 79 | /// // encrypt a public v3 token 80 | /// let token = Paseto::::builder().set_payload(payload).try_encrypt(&key, &nonce)?; 81 | /// # let json = Paseto::::try_decrypt(&token, &key, None, None)?; 82 | /// # assert_eq!(payload, json); 83 | /// # Ok::<(),anyhow::Error>(()) 84 | /// ``` 85 | pub fn try_encrypt( 86 | &mut self, 87 | key: &PasetoSymmetricKey, 88 | nonce: &PasetoNonce, 89 | ) -> Result { 90 | //setup 91 | let footer = self.footer.unwrap_or_default(); 92 | let implicit_assertion = self.implicit_assertion.unwrap_or_default(); 93 | 94 | //split key 95 | let authentication_key = 96 | AuthenticationKey::::try_from(&(AuthenticationKeySeparator::default() + nonce), key)?; 97 | let encryption_key = EncryptionKey::::try_from(&(EncryptionKeySeparator::default() + nonce), key)?; 98 | 99 | //encrypt payload 100 | let ciphertext = CipherText::::from(&self.payload, &encryption_key); 101 | 102 | //pack preauth 103 | let pae = 104 | PreAuthenticationEncoding::parse(&[&self.header, nonce.as_ref(), &ciphertext, &footer, &implicit_assertion]); 105 | 106 | // //generate tag 107 | let tag = Tag::::from(authentication_key, &pae); 108 | 109 | // //generate appended and base64 encoded payload 110 | let raw_payload = RawPayload::::from(nonce, &ciphertext, &tag)?; 111 | 112 | //format as paseto with header and optional footer 113 | Ok(self.format_token(&raw_payload)) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/core/paseto_impl/v3_public.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v3_public")] 2 | 3 | use crate::core::{Footer, Header, ImplicitAssertion, Paseto, PasetoAsymmetricPrivateKey, PasetoAsymmetricPublicKey, PasetoError, Public, V3}; 4 | use crate::core::common::{PreAuthenticationEncoding, RawPayload}; 5 | use p384::ecdsa::{ 6 | signature::DigestSigner, signature::DigestVerifier, Signature, SigningKey, VerifyingKey, 7 | }; 8 | use p384::elliptic_curve::sec1::ToEncodedPoint; 9 | use p384::PublicKey; 10 | use sha2::Digest; 11 | 12 | impl<'a> Paseto<'a, V3, Public> { 13 | /// Verifies a signed V3 Public Paseto 14 | pub fn try_verify( 15 | signature: &'a str, 16 | public_key: &PasetoAsymmetricPublicKey, 17 | footer: (impl Into>> + Copy), 18 | implicit_assertion: (impl Into>> + Copy), 19 | ) -> Result { 20 | let decoded_payload = Self::parse_raw_token(signature, footer, &V3::default(), &Public::default())?; 21 | 22 | //compress the key 23 | let compressed_public_key = PublicKey::from_sec1_bytes(public_key.as_ref()) 24 | .map_err(|_| PasetoError::InvalidKey)? 25 | .to_encoded_point(true); 26 | 27 | let verifying_key = 28 | VerifyingKey::from_sec1_bytes(compressed_public_key.as_ref()).map_err(|_| PasetoError::InvalidKey)?; 29 | let msg = decoded_payload[..(decoded_payload.len() - 96)].as_ref(); 30 | let sig = decoded_payload[msg.len()..msg.len() + 96].as_ref(); 31 | 32 | let signature = Signature::try_from(sig).map_err(|_| PasetoError::Signature)?; 33 | let m2 = PreAuthenticationEncoding::parse(&[ 34 | compressed_public_key.as_ref(), 35 | &Header::::default(), 36 | msg, 37 | &footer.into().unwrap_or_default(), 38 | &implicit_assertion.into().unwrap_or_default(), 39 | ]); 40 | let mut msg_digest = sha2::Sha384::default(); 41 | msg_digest.update(&*m2); 42 | verifying_key 43 | .verify_digest(msg_digest, &signature) 44 | .map_err(|_| PasetoError::InvalidSignature)?; 45 | 46 | Ok(String::from_utf8(Vec::from(msg))?) 47 | } 48 | 49 | /// Attempts to sign a V3 Public Paseto 50 | /// Fails with a PasetoError if the token is malformed or the private key isn't in a valid format 51 | pub fn try_sign(&mut self, key: &PasetoAsymmetricPrivateKey) -> Result { 52 | let footer = self.footer.unwrap_or_default(); 53 | 54 | let implicit_assertion = self.implicit_assertion.unwrap_or_default(); 55 | let signing_key = SigningKey::from_bytes(key.as_ref().into()).map_err(|_| PasetoError::InvalidKey)?; 56 | let public_key = VerifyingKey::from(&signing_key).to_encoded_point(true); 57 | 58 | let m2 = PreAuthenticationEncoding::parse(&[ 59 | public_key.as_ref(), 60 | &self.header, 61 | &self.payload, 62 | &footer, 63 | &implicit_assertion, 64 | ]); 65 | let mut msg_digest = sha2::Sha384::new(); 66 | msg_digest.update(&*m2); 67 | let signature: Signature = signing_key 68 | .try_sign_digest(msg_digest)?; 69 | let raw_payload = RawPayload::::from(&self.payload, &signature.to_bytes()); 70 | Ok(self.format_token(&raw_payload)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/core/paseto_impl/v4_local.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v4_local")] 2 | 3 | use std::str; 4 | 5 | use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals; 6 | 7 | use crate::core::{Footer, Header, ImplicitAssertion, Key, Local, Paseto, PasetoError, PasetoNonce, PasetoSymmetricKey, V4}; 8 | use crate::core::common::{AuthenticationKey, AuthenticationKeySeparator, CipherText, EncryptionKey, EncryptionKeySeparator, PreAuthenticationEncoding, RawPayload, Tag}; 9 | 10 | impl<'a> Paseto<'a, V4, Local> { 11 | /// Attempts to decrypt a PASETO token 12 | /// ``` 13 | /// # use serde_json::json; 14 | /// # use rusty_paseto::core::*; 15 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 16 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 17 | /// # let nonce = PasetoNonce::::from(&nonce); 18 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 19 | /// # let payload = payload.as_str(); 20 | /// # let payload = Payload::from(payload); 21 | /// # let token = Paseto::::builder().set_payload(payload).try_encrypt(&key, &nonce)?; 22 | /// // decrypt a public v4 token 23 | /// let json = Paseto::::try_decrypt(&token, &key, None, None)?; 24 | /// # assert_eq!(payload, json); 25 | /// # Ok::<(),anyhow::Error>(()) 26 | /// ``` 27 | pub fn try_decrypt( 28 | token: &'a str, 29 | key: &PasetoSymmetricKey, 30 | footer: (impl Into>> + Copy), 31 | implicit_assertion: (impl Into>> + Copy), 32 | ) -> Result { 33 | //get footer 34 | 35 | let decoded_payload = Self::parse_raw_token(token, footer, &V4::default(), &Local::default())?; 36 | let nonce = Key::from(&decoded_payload[..32]); 37 | let nonce = PasetoNonce::::from(&nonce); 38 | 39 | let authentication_key = 40 | AuthenticationKey::::from(&(AuthenticationKeySeparator::default() + &nonce), key); 41 | let encryption_key = EncryptionKey::::from(&(EncryptionKeySeparator::default() + &nonce), key); 42 | 43 | let ciphertext = &decoded_payload[32..(decoded_payload.len() - 32)]; 44 | 45 | //pack preauth 46 | let pae = PreAuthenticationEncoding::parse(&[ 47 | &Header::::default(), 48 | nonce.as_ref(), 49 | ciphertext, 50 | &footer.into().unwrap_or_default(), 51 | &implicit_assertion.into().unwrap_or_default(), 52 | ]); 53 | 54 | //generate tags 55 | let tag = &decoded_payload[(nonce.len() + ciphertext.len())..]; 56 | let tag2 = &Tag::::from(authentication_key, &pae); 57 | //compare tags 58 | ConstantTimeEquals(tag, tag2)?; 59 | 60 | //decrypt payload 61 | let ciphertext = CipherText::::from(ciphertext, &encryption_key); 62 | 63 | let decoded_str = str::from_utf8(&ciphertext)?; 64 | 65 | //return decrypted payload 66 | Ok(decoded_str.to_owned()) 67 | } 68 | 69 | /// Attempts to encrypt a PASETO token 70 | /// ``` 71 | /// # use serde_json::json; 72 | /// # use rusty_paseto::core::*; 73 | /// # let key = PasetoSymmetricKey::::from(Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?); 74 | /// # let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 75 | /// # // generate a random nonce with 76 | /// # // let nonce = Key::<32>::try_new_random()?; 77 | /// # let nonce = PasetoNonce::::from(&nonce); 78 | /// # let payload = json!({"data": "this is a secret message", "exp":"2022-01-01T00:00:00+00:00"}).to_string(); 79 | /// # let payload = payload.as_str(); 80 | /// # let payload = Payload::from(payload); 81 | /// //create a public v4 token 82 | /// let token = Paseto::::builder().set_payload(payload).try_encrypt(&key, &nonce)?; 83 | /// # let json = Paseto::::try_decrypt(&token, &key, None, None)?; 84 | /// # assert_eq!(payload, json); 85 | /// # Ok::<(),anyhow::Error>(()) 86 | /// ``` 87 | pub fn try_encrypt( 88 | &mut self, 89 | key: &PasetoSymmetricKey, 90 | nonce: &PasetoNonce, 91 | ) -> Result { 92 | //setup 93 | let footer = self.footer.unwrap_or_default(); 94 | let implicit_assertion = self.implicit_assertion.unwrap_or_default(); 95 | 96 | //split key 97 | let authentication_key = 98 | AuthenticationKey::::from(&(AuthenticationKeySeparator::default() + nonce), key); 99 | let encryption_key = EncryptionKey::::from(&(EncryptionKeySeparator::default() + nonce), key); 100 | 101 | //encrypt payload 102 | let ciphertext = CipherText::::from(&self.payload, &encryption_key); 103 | 104 | //pack preauth 105 | let pae = 106 | PreAuthenticationEncoding::parse(&[&self.header, nonce.as_ref(), &ciphertext, &footer, &implicit_assertion]); 107 | 108 | //generate tag 109 | let tag = Tag::::from(authentication_key, &pae); 110 | 111 | //generate appended and base64 encoded payload 112 | let raw_payload = RawPayload::::try_from(nonce, &ciphertext, &tag)?; 113 | 114 | //format as paseto with header and optional footer 115 | Ok(self.format_token(&raw_payload)) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/core/paseto_impl/v4_public.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "v4_public")] 2 | use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; 3 | use crate::core::{Footer, Header, ImplicitAssertion, Paseto, PasetoAsymmetricPrivateKey, PasetoAsymmetricPublicKey, PasetoError, Public, V4}; 4 | use crate::core::common::{PreAuthenticationEncoding, RawPayload}; 5 | 6 | impl<'a> Paseto<'a, V4, Public> { 7 | pub fn try_verify( 8 | signature: &'a str, 9 | public_key: &PasetoAsymmetricPublicKey, 10 | footer: (impl Into>> + Copy), 11 | implicit_assertion: (impl Into>> + Copy), 12 | ) -> Result { 13 | let decoded_payload = Self::parse_raw_token(signature, footer, &V4::default(), &Public::default())?; 14 | 15 | let verifying_key: VerifyingKey = VerifyingKey::from_bytes(<&[u8; 32]>::try_from(public_key.as_ref())?)?; 16 | 17 | let msg = decoded_payload[..(decoded_payload.len() - ed25519_dalek::SIGNATURE_LENGTH)].as_ref(); 18 | let sig = decoded_payload[msg.len()..msg.len() + ed25519_dalek::SIGNATURE_LENGTH].as_ref(); 19 | 20 | let signature = Signature::try_from(sig)?; 21 | let pae = PreAuthenticationEncoding::parse(&[ 22 | &Header::::default(), 23 | msg, 24 | &footer.into().unwrap_or_default(), 25 | &implicit_assertion.into().unwrap_or_default(), 26 | ]); 27 | 28 | verifying_key.verify(&pae, &signature)?; 29 | // public_key.verify(&pae, &signature)?; 30 | 31 | Ok(String::from_utf8(Vec::from(msg))?) 32 | } 33 | 34 | pub fn try_sign(&mut self, key: &PasetoAsymmetricPrivateKey) -> Result { 35 | let footer = self.footer.unwrap_or_default(); 36 | let assertion = self.implicit_assertion.unwrap_or_default(); 37 | // let secret_key : SecretKey = SecretKey::try_from(key.as_ref())?; 38 | let signing_key = SigningKey::from_keypair_bytes(<&[u8; 64]>::try_from(key.as_ref())?)?; 39 | 40 | // let keypair = Keypair::from_bytes(key.as_ref())?; 41 | 42 | let pae = PreAuthenticationEncoding::parse(&[&self.header, &self.payload, &footer, &assertion]); 43 | 44 | 45 | let signature = signing_key.sign(&pae); 46 | 47 | let raw_payload = RawPayload::::from(&self.payload, &signature.to_bytes()); 48 | 49 | Ok(self.format_token(&raw_payload)) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/core/payload.rs: -------------------------------------------------------------------------------- 1 | use super::traits::Base64Encodable; 2 | use std::fmt; 3 | use std::ops::Deref; 4 | 5 | /// The token payload 6 | #[derive(Default, Debug, Clone, Copy)] 7 | pub struct Payload<'a>(&'a str); 8 | impl Base64Encodable for Payload<'_> {} 9 | 10 | impl<'a> Deref for Payload<'a> { 11 | type Target = [u8]; 12 | 13 | fn deref(&self) -> &'a Self::Target { 14 | self.0.as_bytes() 15 | } 16 | } 17 | 18 | impl<'a> AsRef for Payload<'a> { 19 | fn as_ref(&self) -> &str { 20 | self.0 21 | } 22 | } 23 | 24 | impl<'a> From<&'a str> for Payload<'a> { 25 | fn from(s: &'a str) -> Self { 26 | Self(s) 27 | } 28 | } 29 | 30 | impl<'a, R> PartialEq for Payload<'a> 31 | where 32 | R: AsRef, 33 | { 34 | fn eq(&self, other: &R) -> bool { 35 | self.as_ref() == other.as_ref() 36 | } 37 | } 38 | 39 | impl<'a> fmt::Display for Payload<'a> { 40 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 41 | write!(f, "{}", self.0) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/core/purpose/local.rs: -------------------------------------------------------------------------------- 1 | use crate::core::traits::*; 2 | use std::fmt; 3 | use std::fmt::Display; 4 | 5 | /// Symmetric encryption 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct Local(&'static str); 8 | impl PurposeTrait for Local {} 9 | impl Default for Local { 10 | fn default() -> Self { 11 | Self("local") 12 | } 13 | } 14 | impl AsRef for Local { 15 | fn as_ref(&self) -> &str { 16 | self.0 17 | } 18 | } 19 | impl Display for Local { 20 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 21 | write!(f, "{}", self.0) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/purpose/mod.rs: -------------------------------------------------------------------------------- 1 | mod local; 2 | mod public; 3 | pub use local::Local; 4 | pub use public::Public; 5 | -------------------------------------------------------------------------------- /src/core/purpose/public.rs: -------------------------------------------------------------------------------- 1 | use crate::core::traits::*; 2 | use std::fmt; 3 | use std::fmt::Display; 4 | 5 | /// Asymmetric authentication (public-key signatures) 6 | #[derive(Debug, Clone, Copy)] 7 | pub struct Public(&'static str); 8 | 9 | impl PurposeTrait for Public {} 10 | impl AsRef for Public { 11 | fn as_ref(&self) -> &str { 12 | self.0 13 | } 14 | } 15 | impl Default for Public { 16 | fn default() -> Self { 17 | Self("public") 18 | } 19 | } 20 | 21 | impl Display for Public { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | write!(f, "{}", self.0) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/core/traits.rs: -------------------------------------------------------------------------------- 1 | use base64::DecodeError; 2 | use base64::prelude::*; 3 | use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals; 4 | use std::fmt::Display; 5 | 6 | //marker traits 7 | /// Used by marker traits to determine at compile time which PASETO version the user is attempting to use 8 | pub trait VersionTrait: Display + Default + AsRef {} 9 | /// Used by marker traits to determine at compile time which PASETO purpose the user is attempting to use 10 | pub trait PurposeTrait: Display + Default + AsRef {} 11 | pub trait V1orV3: VersionTrait {} 12 | /// A marker trait used to determine if the PASETO token version is capable of using an implicit 13 | /// assertion. Currently this applies only to V3/V4 PASETO tokens 14 | pub trait ImplicitAssertionCapable: VersionTrait {} 15 | pub trait V2orV4: VersionTrait {} 16 | 17 | /// Enable a type to encode/decode to/from base64 and compare itself to another implementer using 18 | /// constant time comparision 19 | pub(crate) trait Base64Encodable>: Display + AsRef { 20 | fn encode(&self) -> String { 21 | BASE64_URL_SAFE_NO_PAD.encode(self.as_ref()) 22 | } 23 | fn decode(&self) -> Result, DecodeError> { 24 | BASE64_URL_SAFE_NO_PAD.decode(self.as_ref()) 25 | } 26 | fn constant_time_equals(&self, other: B) -> bool 27 | where 28 | B: AsRef, 29 | { 30 | ConstantTimeEquals(self.encode().as_ref(), other.as_ref().as_bytes()).is_ok() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/core/version/mod.rs: -------------------------------------------------------------------------------- 1 | #[cfg(any(feature = "v1", doc))] 2 | mod v1; 3 | #[cfg(any(feature = "v2", doc))] 4 | mod v2; 5 | #[cfg(any(feature = "v3", doc))] 6 | mod v3; 7 | #[cfg(any(feature = "v4", doc))] 8 | mod v4; 9 | 10 | #[cfg(any(feature = "v1", doc))] 11 | pub use v1::V1; 12 | #[cfg(any(feature = "v2", doc))] 13 | pub use v2::V2; 14 | #[cfg(any(feature = "v3", doc))] 15 | pub use v3::V3; 16 | #[cfg(any(feature = "v4", doc))] 17 | pub use v4::V4; 18 | -------------------------------------------------------------------------------- /src/core/version/v1.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(feature = "v1", doc))] 2 | use crate::core::traits::*; 3 | use std::fmt; 4 | use std::fmt::Display; 5 | 6 | /// ## Version 1: NIST Compatibility 7 | /// 8 | /// See [the version 1 specification](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version1.md) for details. At a glance: 9 | /// 10 | /// * **`v1.local`**: Symmetric Authenticated Encryption: 11 | /// * AES-256-CTR + HMAC-SHA384 (Encrypt-then-MAC) 12 | /// * Key-splitting: HKDF-SHA384 13 | /// * Info for encryption key: `paseto-encryption-key` 14 | /// * Info for authentication key: `paseto-auth-key-for-aead` 15 | /// * 32-byte nonce (first half for AES-CTR, latter half for the HKDF salt) 16 | /// * The nonce calculated from HMAC-SHA384(message, `random_bytes(32)`) 17 | /// truncated to 32 bytes, during encryption only 18 | /// * The HMAC covers the header, nonce, and ciphertext 19 | /// * It also covers the footer, if provided 20 | /// * **`v1.public`**: Asymmetric Authentication (Public-Key Signatures): 21 | /// * 2048-bit RSA keys 22 | /// * RSASSA-PSS with 23 | /// * Hash function: SHA384 as the hash function 24 | /// * Mask generation function: MGF1+SHA384 25 | /// * Public exponent: 65537 26 | /// 27 | /// Version 1 implements the best possible RSA + AES + SHA2 ciphersuite. We only use 28 | /// OAEP and PSS for RSA encryption and RSA signatures (respectively), never PKCS1v1.5. 29 | /// 30 | /// Version 1 is recommended only for legacy systems that cannot use modern cryptography. 31 | #[derive(Debug, Clone, Copy)] 32 | pub struct V1(&'static str); 33 | impl AsRef for V1 { 34 | fn as_ref(&self) -> &str { 35 | self.0 36 | } 37 | } 38 | impl V1orV3 for V1 {} 39 | impl VersionTrait for V1 {} 40 | impl Default for V1 { 41 | fn default() -> Self { 42 | Self("v1") 43 | } 44 | } 45 | impl Display for V1 { 46 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 47 | write!(f, "{}", self.0) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/core/version/v2.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(feature = "v2", doc))] 2 | use crate::core::traits::*; 3 | use std::fmt; 4 | use std::fmt::Display; 5 | 6 | /// ## Version 2: Sodium Original 7 | /// 8 | /// See [the version 2 specification](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version2.md) for details. At a glance: 9 | /// 10 | /// * **`v2.local`**: Symmetric Encryption: 11 | /// * XChaCha20-Poly1305 (192-bit nonce, 256-bit key, 128-bit authentication tag) 12 | /// * Encrypting: `sodium_crypto_aead_xchacha20poly1305_ietf_encrypt()` 13 | /// * Decrypting: `sodium_crypto_aead_xchacha20poly1305_ietf_decrypt()` 14 | /// * The nonce is calculated from `sodium_crypto_generichash()` of the message, 15 | /// with a BLAKE2b key provided by `random_bytes(24)` and an output length of 24, 16 | /// during encryption only 17 | /// * Reference implementation in [Version2.php](https://github.com/paragonie/paseto/blob/master/src/Protocol/Version2.php): 18 | /// * See `aeadEncrypt()` for encryption 19 | /// * See `aeadDecrypt()` for decryption 20 | /// * **`v2.public`**: Asymmetric Authentication (Public-Key Signatures): 21 | /// * Ed25519 (EdDSA over Curve25519) 22 | /// * Signing: `sodium_crypto_sign_detached()` 23 | /// * Verifying: `sodium_crypto_sign_verify_detached()` 24 | /// * Reference implementation in [Version2.php](https://github.com/paragonie/paseto/blob/master/src/Protocol/Version2.php): 25 | /// * See `sign()` for signature generation 26 | /// * See `verify()` for signature verification 27 | #[derive(Debug, Clone, Copy)] 28 | pub struct V2(&'static str); 29 | impl VersionTrait for V2 {} 30 | impl AsRef for V2 { 31 | fn as_ref(&self) -> &str { 32 | self.0 33 | } 34 | } 35 | impl V2orV4 for V2 {} 36 | impl Default for V2 { 37 | fn default() -> Self { 38 | Self("v2") 39 | } 40 | } 41 | impl Display for V2 { 42 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 43 | write!(f, "{}", self.0) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/core/version/v3.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(feature = "v3", doc))] 2 | use crate::core::traits::*; 3 | use std::fmt; 4 | use std::fmt::Display; 5 | 6 | /// ## Version 3: NIST Modern 7 | /// 8 | /// See [the version 3 specification](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version3.md) for details. At a glance: 9 | /// 10 | /// * **`v3.local`**: Symmetric Authenticated Encryption: 11 | /// * AES-256-CTR + HMAC-SHA384 (Encrypt-then-MAC) 12 | /// * Key-splitting: HKDF-SHA384 13 | /// * Info for encryption key: `paseto-encryption-key` 14 | /// The encryption key and implicit counter nonce are both returned 15 | /// from HKDF in this version. 16 | /// * Info for authentication key: `paseto-auth-key-for-aead` 17 | /// * 32-byte nonce (no longer prehashed), passed entirely to HKDF 18 | /// (as part of the `info` tag, rather than as a salt). 19 | /// * The HMAC covers the header, nonce, and ciphertext 20 | /// * It also covers the footer, if provided 21 | /// * It also covers the implicit assertions, if provided 22 | /// * **`v3.public`**: Asymmetric Authentication (Public-Key Signatures): 23 | /// * ECDSA over NIST P-384, with SHA-384, 24 | /// using [RFC 6979 deterministic k-values](https://tools.ietf.org/html/rfc6979) 25 | /// (if reasonably practical; otherwise a CSPRNG **MUST** be used). 26 | /// Hedged signatures are allowed too. 27 | /// * The public key is also included in the PAE step, to ensure 28 | /// `v3.public` tokens provide Exclusive Ownership. 29 | #[derive(Debug, Clone, Copy)] 30 | pub struct V3(&'static str); 31 | impl VersionTrait for V3 {} 32 | impl AsRef for V3 { 33 | fn as_ref(&self) -> &str { 34 | self.0 35 | } 36 | } 37 | impl ImplicitAssertionCapable for V3 {} 38 | impl V1orV3 for V3 {} 39 | impl Default for V3 { 40 | fn default() -> Self { 41 | Self("v3") 42 | } 43 | } 44 | impl Display for V3 { 45 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | write!(f, "{}", self.0) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/core/version/v4.rs: -------------------------------------------------------------------------------- 1 | #![cfg(any(feature = "v4", doc))] 2 | use crate::core::traits::*; 3 | use std::fmt; 4 | use std::fmt::Display; 5 | 6 | /// ## Version 4: Sodium Modern 7 | /// 8 | /// See [the version 4 specification](https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md) for details. At a glance: 9 | /// 10 | /// * **`v4.local`**: Symmetric Authenticated Encryption: 11 | /// * XChaCha20 + BLAKE2b-MAC (Encrypt-then-MAC) 12 | /// * Key-splitting: BLAKE2b 13 | /// * Info for encryption key: `paseto-encryption-key` 14 | /// The encryption key and implicit counter nonce are both returned 15 | /// from BLAKE2b in this version. 16 | /// * Info for authentication key: `paseto-auth-key-for-aead` 17 | /// * 32-byte nonce (no longer prehashed), passed entirely to BLAKE2b. 18 | /// * The BLAKE2b-MAC covers the header, nonce, and ciphertext 19 | /// * It also covers the footer, if provided 20 | /// * It also covers the implicit assertions, if provided 21 | /// * **`v4.public`**: Asymmetric Authentication (Public-Key Signatures): 22 | /// * Ed25519 (EdDSA over Curve25519) 23 | /// * Signing: `sodium_crypto_sign_detached()` 24 | /// * Verifying: `sodium_crypto_sign_verify_detached()` 25 | #[derive(Debug, Clone, Copy)] 26 | pub struct V4(&'static str); 27 | impl VersionTrait for V4 {} 28 | impl ImplicitAssertionCapable for V4 {} 29 | impl V2orV4 for V4 {} 30 | impl AsRef for V4 { 31 | fn as_ref(&self) -> &str { 32 | self.0 33 | } 34 | } 35 | impl Default for V4 { 36 | fn default() -> Self { 37 | Self("v4") 38 | } 39 | } 40 | impl Display for V4 { 41 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 42 | write!(f, "{}", self.0) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/generic/builders/error.rs: -------------------------------------------------------------------------------- 1 | use crate::generic::PasetoClaimError; 2 | use thiserror::Error; 3 | /// Errors raised by the generic builder when adding claims or encrypting or signing PASETO tokens. 4 | #[derive(Debug, Error)] 5 | pub enum GenericBuilderError { 6 | /// A generic claim error 7 | #[error(transparent)] 8 | ClaimError { 9 | #[from] 10 | source: PasetoClaimError, 11 | }, 12 | ///An error with a invalid malformed iso8601 email address 13 | #[error("{0} is an invalid iso8601 (email) string")] 14 | BadEmailAddress(String), 15 | ///An error indicating a duplicate top level claim in the token 16 | #[error("The claim '{0}' appears more than once in the top level payload json")] 17 | DuplicateTopLevelPayloadClaim(String), 18 | ///A generic cipher error 19 | #[error("A paseto cipher error occurred")] 20 | CipherError { 21 | #[from] 22 | source: crate::core::PasetoError, 23 | }, 24 | ///A JSON serialization error with the token payload 25 | #[error("The payload was unable to be serialized into json")] 26 | PayloadJsonError { 27 | #[from] 28 | source: serde_json::Error, 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /src/generic/builders/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod generic_builder; 3 | mod traits; 4 | pub use error::GenericBuilderError; 5 | pub use generic_builder::GenericBuilder; 6 | //pub use traits::*; 7 | -------------------------------------------------------------------------------- /src/generic/builders/traits.rs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/src/generic/builders/traits.rs -------------------------------------------------------------------------------- /src/generic/claims/audience_claim.rs: -------------------------------------------------------------------------------- 1 | use super::PasetoClaim; 2 | #[cfg(feature = "serde")] 3 | use serde::ser::SerializeMap; 4 | 5 | ///The reserved ['aud'](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) PASETO claim 6 | #[derive(Clone)] 7 | pub struct AudienceClaim<'a>((&'a str, &'a str)); 8 | impl<'a> PasetoClaim for AudienceClaim<'a> { 9 | fn get_key(&self) -> &str { 10 | self.0 .0 11 | } 12 | } 13 | 14 | impl<'a> Default for AudienceClaim<'a> { 15 | fn default() -> Self { 16 | Self(("aud", "")) 17 | } 18 | } 19 | 20 | //created using the From trait 21 | impl<'a> From<&'a str> for AudienceClaim<'a> { 22 | fn from(s: &'a str) -> Self { 23 | Self(("aud", s)) 24 | } 25 | } 26 | 27 | //want to receive a reference as a tuple 28 | impl<'a> AsRef<(&'a str, &'a str)> for AudienceClaim<'a> { 29 | fn as_ref(&self) -> &(&'a str, &'a str) { 30 | &self.0 31 | } 32 | } 33 | 34 | #[cfg(feature = "serde")] 35 | impl<'a> serde::Serialize for AudienceClaim<'a> { 36 | fn serialize(&self, serializer: S) -> Result 37 | where 38 | S: serde::Serializer, 39 | { 40 | let mut map = serializer.serialize_map(Some(2))?; 41 | map.serialize_key(&self.0 .0)?; 42 | map.serialize_value(&self.0 .1)?; 43 | //map.serialize_entry(self.0 .0, self.0 .1)?; 44 | map.end() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/generic/claims/custom_claim.rs: -------------------------------------------------------------------------------- 1 | use super::{PasetoClaim, PasetoClaimError}; 2 | #[cfg(feature = "serde")] 3 | use serde::ser::SerializeMap; 4 | 5 | ///A custom PASETO claim which can be created with a key and a value T 6 | /// ## Setting your own Custom Claims 7 | /// 8 | /// The CustomClaim struct takes a tuple in the form of `(key: String, value: T)` where T is any 9 | /// serializable type 10 | /// #### Note: *CustomClaims use the TryFrom trait and return a Result<(), PasetoClaimError> if you attempt to use one of the [reserved PASETO keys](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) in your CustomClaim* 11 | /// 12 | /// ```rust 13 | /// # use rusty_paseto::prelude::*; 14 | /// # #[cfg(feature = "default")] 15 | /// # { 16 | /// # // must include 17 | /// # use std::convert::TryFrom; 18 | /// # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); 19 | /// let token = PasetoBuilder::::default() 20 | /// .set_claim(CustomClaim::try_from(("Co-star", "Morty Smith"))?) 21 | /// .set_claim(CustomClaim::try_from(("Universe", 137))?) 22 | /// .build(&key)?; 23 | /// # } 24 | /// # Ok::<(),GenericBuilderError>(()) 25 | /// ``` 26 | /// 27 | /// This throws an error: 28 | /// ```should_panic 29 | /// # use rusty_paseto::prelude::*; 30 | /// # #[cfg(feature = "default")] 31 | /// # { 32 | /// # // must include 33 | /// # use std::convert::TryFrom; 34 | /// # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); 35 | /// // "exp" is a reserved PASETO claim key, you should use the ExpirationClaim type 36 | /// let token = PasetoBuilder::::default() 37 | /// .set_claim(CustomClaim::try_from(("exp", "Some expiration value"))?) 38 | /// .build(&key)?; 39 | /// # } 40 | /// # Ok::<(),anyhow::Error>(()) 41 | /// ``` 42 | /// # Validating claims 43 | /// rusty_paseto allows for flexible claim validation at parse time 44 | /// 45 | /// ## Checking claims 46 | /// 47 | /// Let's see how we can check particular claims exist with expected values. 48 | /// ``` 49 | /// # #[cfg(feature = "default")] 50 | /// # { 51 | /// # use rusty_paseto::prelude::*; 52 | /// # use std::convert::TryFrom; 53 | /// 54 | /// # // create a key specifying the PASETO version and purpose 55 | /// # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); 56 | /// // use a default token builder with the same PASETO version and purpose 57 | /// let token = PasetoBuilder::::default() 58 | /// .set_claim(SubjectClaim::from("Get schwifty")) 59 | /// .set_claim(CustomClaim::try_from(("Contestant", "Earth"))?) 60 | /// .set_claim(CustomClaim::try_from(("Universe", 137))?) 61 | /// .build(&key)?; 62 | /// 63 | /// PasetoParser::::default() 64 | /// // you can check any claim even custom claims 65 | /// .check_claim(SubjectClaim::from("Get schwifty")) 66 | /// .check_claim(CustomClaim::try_from(("Contestant", "Earth"))?) 67 | /// .check_claim(CustomClaim::try_from(("Universe", 137))?) 68 | /// .parse(&token, &key)?; 69 | /// 70 | /// // no need for the assertions below since the check_claim methods 71 | /// // above accomplish the same but at parse time! 72 | /// 73 | /// //assert_eq!(json_value["sub"], "Get schwifty"); 74 | /// //assert_eq!(json_value["Contestant"], "Earth"); 75 | /// //assert_eq!(json_value["Universe"], 137); 76 | /// # } 77 | /// # Ok::<(),anyhow::Error>(()) 78 | /// ``` 79 | /// 80 | /// # Custom validation 81 | /// 82 | /// What if we have more complex validation requirements? You can pass in a reference to a closure which receives 83 | /// the key and value of the claim you want to validate so you can implement any validation logic 84 | /// you choose. 85 | /// 86 | /// Let's see how we can validate our tokens only contain universes with prime numbers: 87 | /// 88 | /// ``` 89 | /// # use rusty_paseto::prelude::*; 90 | /// # #[cfg(feature = "default")] 91 | /// # { 92 | /// # use std::convert::TryFrom; 93 | /// 94 | /// # // create a key specifying the PASETO version and purpose 95 | /// # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); 96 | /// // use a default token builder with the same PASETO version and purpose 97 | /// let token = PasetoBuilder::::default() 98 | /// .set_claim(SubjectClaim::from("Get schwifty")) 99 | /// .set_claim(CustomClaim::try_from(("Contestant", "Earth"))?) 100 | /// .set_claim(CustomClaim::try_from(("Universe", 137))?) 101 | /// .build(&key)?; 102 | /// 103 | /// PasetoParser::::default() 104 | /// .check_claim(SubjectClaim::from("Get schwifty")) 105 | /// .check_claim(CustomClaim::try_from(("Contestant", "Earth"))?) 106 | /// .validate_claim(CustomClaim::try_from("Universe")?, &|key, value| { 107 | /// //let's get the value 108 | /// let universe = value 109 | /// .as_u64() 110 | /// .ok_or(PasetoClaimError::Unexpected(key.to_string()))?; 111 | /// // we only accept prime universes in this app 112 | /// if primes::is_prime(universe) { 113 | /// Ok(()) 114 | /// } else { 115 | /// Err(PasetoClaimError::CustomValidation(key.to_string())) 116 | /// } 117 | /// }) 118 | /// .parse(&token, &key)?; 119 | /// # } 120 | /// # Ok::<(),anyhow::Error>(()) 121 | /// ``` 122 | /// 123 | /// This token will fail to parse with the validation code above: 124 | /// ```should_panic 125 | /// # #[cfg(feature = "default")] 126 | /// # { 127 | /// # use rusty_paseto::prelude::*; 128 | /// # use std::convert::TryFrom; 129 | /// 130 | /// # // create a key specifying the PASETO version and purpose 131 | /// # let key = PasetoSymmetricKey::::from(Key::from(b"wubbalubbadubdubwubbalubbadubdub")); 132 | /// // 136 is not a prime number 133 | /// let token = PasetoBuilder::::default() 134 | /// .set_claim(CustomClaim::try_from(("Universe", 136))?) 135 | /// .build(&key)?; 136 | /// 137 | ///# let json_value = PasetoParser::::default() 138 | ///# // you can check any claim even custom claims 139 | ///# .validate_claim(CustomClaim::try_from("Universe")?, &|key, value| { 140 | ///# //let's get the value 141 | ///# let universe = value 142 | ///# .as_u64() 143 | ///# .ok_or(PasetoClaimError::Unexpected(key.to_string()))?; 144 | ///# // we only accept prime universes in this token 145 | ///# if primes::is_prime(universe) { 146 | ///# Ok(()) 147 | ///# } else { 148 | ///# Err(PasetoClaimError::CustomValidation(key.to_string())) 149 | ///# } 150 | ///# }) 151 | /// 152 | ///# .parse(&token, &key)?; 153 | /// 154 | /// # assert_eq!(json_value["Universe"], 136); 155 | /// # } 156 | /// # Ok::<(),anyhow::Error>(()) 157 | /// ``` 158 | 159 | #[derive(Clone, Debug)] 160 | pub struct CustomClaim((String, T)); 161 | 162 | impl CustomClaim { 163 | //TODO: this needs to be refactored to be configurable for eventual compressed token 164 | //implementations 165 | pub(self) const RESERVED_CLAIMS: [&'static str; 7] = ["iss", "sub", "aud", "exp", "nbf", "iat", "jti"]; 166 | 167 | fn check_if_reserved_claim_key(key: &str) -> Result<(), PasetoClaimError> { 168 | match key { 169 | key if Self::RESERVED_CLAIMS.contains(&key) => Err(PasetoClaimError::Reserved(key.into())), 170 | _ => Ok(()), 171 | } 172 | } 173 | } 174 | 175 | #[cfg(feature = "serde")] 176 | impl PasetoClaim for CustomClaim { 177 | fn get_key(&self) -> &str { 178 | &self.0 .0 179 | } 180 | } 181 | 182 | impl TryFrom<&str> for CustomClaim<&str> { 183 | type Error = PasetoClaimError; 184 | 185 | fn try_from(key: &str) -> Result { 186 | Self::check_if_reserved_claim_key(key)?; 187 | Ok(Self((String::from(key), ""))) 188 | } 189 | } 190 | 191 | impl TryFrom<(String, T)> for CustomClaim { 192 | type Error = PasetoClaimError; 193 | 194 | fn try_from(val: (String, T)) -> Result { 195 | Self::check_if_reserved_claim_key(val.0.as_str())?; 196 | Ok(Self((val.0, val.1))) 197 | } 198 | } 199 | 200 | impl TryFrom<(&str, T)> for CustomClaim { 201 | type Error = PasetoClaimError; 202 | 203 | fn try_from(val: (&str, T)) -> Result { 204 | Self::check_if_reserved_claim_key(val.0)?; 205 | Ok(Self((String::from(val.0), val.1))) 206 | } 207 | } 208 | 209 | //we want to receive a reference as a tuple 210 | impl AsRef<(String, T)> for CustomClaim { 211 | fn as_ref(&self) -> &(String, T) { 212 | &self.0 213 | } 214 | } 215 | 216 | #[cfg(feature = "serde")] 217 | impl serde::Serialize for CustomClaim { 218 | fn serialize(&self, serializer: S) -> Result 219 | where 220 | S: serde::Serializer, 221 | { 222 | let mut map = serializer.serialize_map(Some(2))?; 223 | map.serialize_key(&self.0 .0)?; 224 | map.serialize_value(&self.0 .1)?; 225 | map.end() 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/generic/claims/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Errors from validating claims in a parsed token 4 | #[derive(Debug, Error)] 5 | pub enum PasetoClaimError { 6 | /// Occurs during an attempt to parse an expired token 7 | #[error("This token is expired")] 8 | Expired, 9 | /// Occurs during an attempt to parse a token before its Not Before claim time 10 | #[error("The token cannot be used before {0}")] 11 | UseBeforeAvailable(String), 12 | /// Occurs if a date time is passed that is not a valid RFC3339 date - "2019-01-01T00:00:00+00:00" 13 | #[error("The value {0} is a malformed RFC3339 date")] 14 | RFC3339Date(String), 15 | /// Occurs if a claim was expected but wasn't found in the payload 16 | #[error("The expected claim '{0}' was not found in the payload")] 17 | Missing(String), 18 | /// Occurs during claim validation if a claim value was unable to be converted to its expected type 19 | #[error("Could not convert claim '{0}' to the expected data type")] 20 | Unexpected(String), 21 | /// Occurs when a custom claim fails validation 22 | #[error("The claim '{0}' failed custom validation")] 23 | CustomValidation(String), 24 | /// Occurs when a claim fails validation 25 | #[error("The claim '{0}' failed validation. Expected '{1}' but received '{2}'")] 26 | Invalid(String, String, String), 27 | /// Occurs when a user attempts to create a custom claim using a reserved claim key 28 | #[error("The key {0} is a reserved for use within PASETO. To set a reserved claim, use the strong type: e.g - ExpirationClaimClaim")] 29 | Reserved(String), 30 | /// Occurs when a user attempts to use a top level claim more than once in the payload 31 | #[error("The claim '{0}' appears more than once in the top level payload json")] 32 | DuplicateTopLevelPayloadClaim(String), 33 | } 34 | -------------------------------------------------------------------------------- /src/generic/claims/expiration_claim.rs: -------------------------------------------------------------------------------- 1 | use super::{PasetoClaim, PasetoClaimError}; 2 | #[cfg(feature = "serde")] 3 | use serde::ser::SerializeMap; 4 | 5 | ///The reserved ['exp'](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) PASETO claim 6 | #[derive(Clone)] 7 | pub struct ExpirationClaim((String, String)); 8 | impl PasetoClaim for ExpirationClaim { 9 | fn get_key(&self) -> &str { 10 | self.0 .0.as_str() 11 | } 12 | } 13 | 14 | impl Default for ExpirationClaim { 15 | fn default() -> Self { 16 | Self(("exp".to_string(), "2019-01-01T00:00:00+00:00".to_string())) 17 | } 18 | } 19 | 20 | impl TryFrom for ExpirationClaim { 21 | type Error = PasetoClaimError; 22 | 23 | fn try_from(value: String) -> Result { 24 | match iso8601::datetime(&value) { 25 | Ok(_) => Ok(Self(("exp".to_string(), value))), 26 | Err(_) => Err(PasetoClaimError::RFC3339Date(value.to_string())), 27 | } 28 | } 29 | } 30 | 31 | impl TryFrom<&str> for ExpirationClaim { 32 | type Error = PasetoClaimError; 33 | 34 | fn try_from(value: &str) -> Result { 35 | match iso8601::datetime(value) { 36 | Ok(_) => Ok(Self(("exp".to_string(), value.to_string()))), 37 | Err(_) => Err(PasetoClaimError::RFC3339Date(value.to_string())), 38 | } 39 | } 40 | } 41 | 42 | //want to receive a reference as a tuple 43 | impl AsRef<(String, String)> for ExpirationClaim { 44 | fn as_ref(&self) -> &(String, String) { 45 | &self.0 46 | } 47 | } 48 | 49 | #[cfg(feature = "serde")] 50 | impl serde::Serialize for ExpirationClaim { 51 | fn serialize(&self, serializer: S) -> Result 52 | where 53 | S: serde::Serializer, 54 | { 55 | let mut map = serializer.serialize_map(Some(2))?; 56 | map.serialize_key(&self.0 .0)?; 57 | map.serialize_value(&self.0 .1)?; 58 | map.end() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/generic/claims/issued_at_claim.rs: -------------------------------------------------------------------------------- 1 | use super::{PasetoClaim, PasetoClaimError}; 2 | #[cfg(feature = "serde")] 3 | use serde::ser::SerializeMap; 4 | 5 | ///The reserved ['iat'](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) PASETO claim 6 | #[derive(Clone)] 7 | pub struct IssuedAtClaim((String, String)); 8 | impl PasetoClaim for IssuedAtClaim { 9 | fn get_key(&self) -> &str { 10 | &self.0 .0 11 | } 12 | } 13 | 14 | impl Default for IssuedAtClaim { 15 | fn default() -> Self { 16 | Self(("iat".to_string(), "2019-01-01T00:00:00+00:00".to_string())) 17 | } 18 | } 19 | 20 | impl TryFrom<&str> for IssuedAtClaim { 21 | type Error = PasetoClaimError; 22 | 23 | fn try_from(value: &str) -> Result { 24 | match iso8601::datetime(value) { 25 | Ok(_) => Ok(Self(("iat".to_string(), value.to_string()))), 26 | Err(_) => Err(PasetoClaimError::RFC3339Date(value.to_string())), 27 | } 28 | } 29 | } 30 | 31 | //want to receive a reference as a tuple 32 | impl AsRef<(String, String)> for IssuedAtClaim { 33 | fn as_ref(&self) -> &(String, String) { 34 | &self.0 35 | } 36 | } 37 | 38 | impl TryFrom for IssuedAtClaim { 39 | type Error = PasetoClaimError; 40 | 41 | fn try_from(value: String) -> Result { 42 | match iso8601::datetime(&value) { 43 | Ok(_) => Ok(Self(("iat".to_string(), value))), 44 | Err(_) => Err(PasetoClaimError::RFC3339Date(value.to_string())), 45 | } 46 | } 47 | } 48 | 49 | #[cfg(feature = "serde")] 50 | impl serde::Serialize for IssuedAtClaim { 51 | fn serialize(&self, serializer: S) -> Result 52 | where 53 | S: serde::Serializer, 54 | { 55 | let mut map = serializer.serialize_map(Some(2))?; 56 | map.serialize_key(&self.0 .0)?; 57 | map.serialize_value(&self.0 .1)?; 58 | map.end() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/generic/claims/issuer_claim.rs: -------------------------------------------------------------------------------- 1 | use super::PasetoClaim; 2 | #[cfg(feature = "serde")] 3 | use serde::ser::SerializeMap; 4 | 5 | ///The reserved ['iss'](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) PASETO claim 6 | #[derive(Clone)] 7 | pub struct IssuerClaim<'a>((&'a str, &'a str)); 8 | 9 | impl<'a> PasetoClaim for IssuerClaim<'a> { 10 | fn get_key(&self) -> &str { 11 | self.0 .0 12 | } 13 | } 14 | 15 | impl<'a> Default for IssuerClaim<'a> { 16 | fn default() -> Self { 17 | Self(("iss", "")) 18 | } 19 | } 20 | 21 | //created using the From trait 22 | impl<'a> From<&'a str> for IssuerClaim<'a> { 23 | fn from(s: &'a str) -> Self { 24 | Self(("iss", s)) 25 | } 26 | } 27 | 28 | //want to receive a reference as a tuple 29 | impl<'a> AsRef<(&'a str, &'a str)> for IssuerClaim<'a> { 30 | fn as_ref(&self) -> &(&'a str, &'a str) { 31 | &self.0 32 | } 33 | } 34 | 35 | #[cfg(feature = "serde")] 36 | impl<'a> serde::Serialize for IssuerClaim<'a> { 37 | fn serialize(&self, serializer: S) -> Result 38 | where 39 | S: serde::Serializer, 40 | { 41 | let mut map = serializer.serialize_map(Some(2))?; 42 | map.serialize_entry(self.0 .0, self.0 .1)?; 43 | map.end() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/generic/claims/mod.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use std::collections::HashMap; 3 | 4 | mod audience_claim; 5 | mod custom_claim; 6 | mod error; 7 | mod expiration_claim; 8 | mod issued_at_claim; 9 | mod issuer_claim; 10 | mod not_before_claim; 11 | mod subject_claim; 12 | mod token_identifier_claim; 13 | mod traits; 14 | 15 | pub use audience_claim::AudienceClaim; 16 | pub use custom_claim::CustomClaim; 17 | pub use error::PasetoClaimError; 18 | pub use expiration_claim::ExpirationClaim; 19 | pub use issued_at_claim::IssuedAtClaim; 20 | pub use issuer_claim::IssuerClaim; 21 | pub use not_before_claim::NotBeforeClaim; 22 | pub use subject_claim::SubjectClaim; 23 | pub use token_identifier_claim::TokenIdentifierClaim; 24 | pub use traits::PasetoClaim; 25 | ///A type for creating generic claim validation functions 26 | pub type ValidatorFn = dyn Fn(&str, &Value) -> Result<(), PasetoClaimError>; 27 | ///A type for tracking claims in a token 28 | pub type ValidatorMap = HashMap>; 29 | 30 | #[cfg(test)] 31 | mod unit_tests { 32 | //TODO: need more comprehensive tests than these to flesh out the additionl error types 33 | use super::*; 34 | use anyhow::Result; 35 | //use chrono::prelude::*; 36 | use std::convert::TryFrom; 37 | use time::format_description::well_known::Rfc3339; 38 | 39 | #[test] 40 | fn test_expiration_claim() -> Result<()> { 41 | // setup 42 | // a good time format 43 | let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; 44 | 45 | assert!(ExpirationClaim::try_from("hello").is_err()); 46 | let claim = ExpirationClaim::try_from(now); 47 | assert!(claim.is_ok()); 48 | let claim = claim.unwrap(); 49 | 50 | assert_eq!(claim.get_key(), "exp"); 51 | 52 | Ok(()) 53 | } 54 | 55 | #[test] 56 | fn test_not_before_claim() -> Result<()> { 57 | // setup 58 | // a good time format 59 | let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; 60 | 61 | assert!(NotBeforeClaim::try_from("hello").is_err()); 62 | let claim = NotBeforeClaim::try_from(now); 63 | assert!(claim.is_ok()); 64 | let claim = claim.unwrap(); 65 | 66 | assert_eq!(claim.get_key(), "nbf"); 67 | 68 | Ok(()) 69 | } 70 | 71 | #[test] 72 | fn test_issued_at_claim() -> Result<()> { 73 | // setup 74 | // a good time format 75 | let now = time::OffsetDateTime::now_utc().format(&Rfc3339)?; 76 | 77 | assert!(IssuedAtClaim::try_from("hello").is_err()); 78 | let claim = IssuedAtClaim::try_from(now); 79 | assert!(claim.is_ok()); 80 | let claim = claim.unwrap(); 81 | 82 | assert_eq!(claim.get_key(), "iat"); 83 | 84 | Ok(()) 85 | } 86 | #[test] 87 | fn test_token_identifier_claim() { 88 | // setup 89 | let borrowed_str = String::from("hello world"); 90 | let claim = TokenIdentifierClaim::from(borrowed_str.as_str()); 91 | 92 | //verify 93 | assert_eq!("jti", claim.get_key()); 94 | } 95 | 96 | #[test] 97 | fn test_audience_claim() { 98 | // setup 99 | let borrowed_str = String::from("hello world"); 100 | let claim = AudienceClaim::from(borrowed_str.as_str()); 101 | 102 | //verify 103 | assert_eq!("aud", claim.get_key()); 104 | } 105 | 106 | #[test] 107 | fn test_subject_claim() { 108 | // setup 109 | let borrowed_str = String::from("hello world"); 110 | let claim = SubjectClaim::from(borrowed_str.as_str()); 111 | 112 | //verify 113 | assert_eq!("sub", claim.get_key()); 114 | } 115 | 116 | #[test] 117 | fn test_iss_claim() { 118 | // setup 119 | let borrowed_str = String::from("hello world"); 120 | let claim = IssuerClaim::from(borrowed_str.as_str()); 121 | 122 | //verify 123 | assert_eq!("iss", claim.get_key()); 124 | } 125 | 126 | #[test] 127 | fn test_basic_custom_claim() -> Result<()> { 128 | let borrowed_str = String::from("universe"); 129 | let claim = CustomClaim::try_from((borrowed_str.as_str(), 137))?; 130 | // setup 131 | //verify 132 | 133 | assert_eq!(claim.get_key(), "universe"); 134 | let (_, v) = claim.as_ref(); 135 | assert_eq!(v, &137); 136 | Ok(()) 137 | } 138 | 139 | #[test] 140 | fn test_restricted_custom_claim() { 141 | // setup 142 | //verify 143 | assert!(CustomClaim::try_from(("iss", 137)).is_err()); 144 | assert!(CustomClaim::try_from(("sub", 137)).is_err()); 145 | assert!(CustomClaim::try_from(("aud", 137)).is_err()); 146 | assert!(CustomClaim::try_from(("exp", 137)).is_err()); 147 | assert!(CustomClaim::try_from(("nbf", 137)).is_err()); 148 | assert!(CustomClaim::try_from(("iat", 137)).is_err()); 149 | assert!(CustomClaim::try_from(("jti", 137)).is_err()); 150 | assert!(CustomClaim::try_from(("i'm good tho", true)).is_ok()); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/generic/claims/not_before_claim.rs: -------------------------------------------------------------------------------- 1 | use super::{PasetoClaim, PasetoClaimError}; 2 | #[cfg(feature = "serde")] 3 | use serde::ser::SerializeMap; 4 | 5 | ///The reserved ['nbf'](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) PASETO claim 6 | #[derive(Clone)] 7 | pub struct NotBeforeClaim((String, String)); 8 | impl PasetoClaim for NotBeforeClaim { 9 | fn get_key(&self) -> &str { 10 | &self.0 .0 11 | } 12 | } 13 | 14 | impl Default for NotBeforeClaim { 15 | fn default() -> Self { 16 | Self(("nbf".to_string(), "2019-01-01T00:00:00+00:00".to_string())) 17 | } 18 | } 19 | 20 | impl TryFrom for NotBeforeClaim { 21 | type Error = PasetoClaimError; 22 | 23 | fn try_from(value: String) -> Result { 24 | match iso8601::datetime(&value) { 25 | Ok(_) => Ok(Self(("nbf".to_string(), value))), 26 | Err(_) => Err(PasetoClaimError::RFC3339Date(value.to_string())), 27 | } 28 | } 29 | } 30 | 31 | impl TryFrom<&str> for NotBeforeClaim { 32 | type Error = PasetoClaimError; 33 | 34 | fn try_from(value: &str) -> Result { 35 | match iso8601::datetime(value) { 36 | Ok(_) => Ok(Self(("nbf".to_string(), value.to_string()))), 37 | Err(_) => Err(PasetoClaimError::RFC3339Date(value.to_string())), 38 | } 39 | } 40 | } 41 | 42 | //want to receive a reference as a tuple 43 | impl AsRef<(String, String)> for NotBeforeClaim { 44 | fn as_ref(&self) -> &(String, String) { 45 | &self.0 46 | } 47 | } 48 | 49 | #[cfg(feature = "serde")] 50 | impl serde::Serialize for NotBeforeClaim { 51 | fn serialize(&self, serializer: S) -> Result 52 | where 53 | S: serde::Serializer, 54 | { 55 | let mut map = serializer.serialize_map(Some(2))?; 56 | map.serialize_key(&self.0 .0)?; 57 | map.serialize_value(&self.0 .1)?; 58 | map.end() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/generic/claims/subject_claim.rs: -------------------------------------------------------------------------------- 1 | use super::PasetoClaim; 2 | #[cfg(feature = "serde")] 3 | use serde::ser::SerializeMap; 4 | 5 | ///The reserved ['sub'](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) PASETO claim 6 | #[derive(Clone)] 7 | pub struct SubjectClaim<'a>((&'a str, &'a str)); 8 | 9 | impl<'a> PasetoClaim for SubjectClaim<'a> { 10 | fn get_key(&self) -> &str { 11 | self.0 .0 12 | } 13 | } 14 | 15 | impl<'a> Default for SubjectClaim<'a> { 16 | fn default() -> Self { 17 | Self(("sub", "")) 18 | } 19 | } 20 | 21 | //created using the From trait 22 | impl<'a> From<&'a str> for SubjectClaim<'a> { 23 | fn from(s: &'a str) -> Self { 24 | Self(("sub", s)) 25 | } 26 | } 27 | 28 | //want to receive a reference as a tuple 29 | impl<'a> AsRef<(&'a str, &'a str)> for SubjectClaim<'a> { 30 | fn as_ref(&self) -> &(&'a str, &'a str) { 31 | &self.0 32 | } 33 | } 34 | 35 | #[cfg(feature = "serde")] 36 | impl<'a> serde::Serialize for SubjectClaim<'a> { 37 | fn serialize(&self, serializer: S) -> Result 38 | where 39 | S: serde::Serializer, 40 | { 41 | let mut map = serializer.serialize_map(Some(2))?; 42 | map.serialize_entry(self.0 .0, self.0 .1)?; 43 | map.end() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/generic/claims/token_identifier_claim.rs: -------------------------------------------------------------------------------- 1 | use super::PasetoClaim; 2 | #[cfg(feature = "serde")] 3 | use serde::ser::SerializeMap; 4 | 5 | ///The reserved ['jti'](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) PASETO claim 6 | #[derive(Clone)] 7 | pub struct TokenIdentifierClaim<'a>((&'a str, &'a str)); 8 | impl<'a> PasetoClaim for TokenIdentifierClaim<'a> { 9 | fn get_key(&self) -> &str { 10 | self.0 .0 11 | } 12 | } 13 | 14 | impl<'a> Default for TokenIdentifierClaim<'a> { 15 | fn default() -> Self { 16 | Self(("jti", "")) 17 | } 18 | } 19 | 20 | //created using the From trait 21 | impl<'a> From<&'a str> for TokenIdentifierClaim<'a> { 22 | fn from(s: &'a str) -> Self { 23 | Self(("jti", s)) 24 | } 25 | } 26 | 27 | //want to receive a reference as a tuple 28 | impl<'a> AsRef<(&'a str, &'a str)> for TokenIdentifierClaim<'a> { 29 | fn as_ref(&self) -> &(&'a str, &'a str) { 30 | &self.0 31 | } 32 | } 33 | 34 | #[cfg(feature = "serde")] 35 | impl<'a> serde::Serialize for TokenIdentifierClaim<'a> { 36 | fn serialize(&self, serializer: S) -> Result 37 | where 38 | S: serde::Serializer, 39 | { 40 | let mut map = serializer.serialize_map(Some(2))?; 41 | map.serialize_key(&self.0 .0)?; 42 | map.serialize_value(&self.0 .1)?; 43 | //map.serialize_entry(self.0 .0, self.0 .1)?; 44 | map.end() 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/generic/claims/traits.rs: -------------------------------------------------------------------------------- 1 | /// a simple marker trait to identify claims 2 | pub trait PasetoClaim: erased_serde::Serialize { 3 | fn get_key(&self) -> &str; 4 | } 5 | -------------------------------------------------------------------------------- /src/generic/mod.rs: -------------------------------------------------------------------------------- 1 | //! The generic architectural and feature layer allows you to create your own custom version of the batteries_included layer by following the same pattern I've used in the source code to create your own custom builder and parser. This is probably not what you need as it is for advanced usage. The feature includes a generic builder and parser along with claims for you to extend. 2 | //! 3 | //! ![paseto_generic_small](https://user-images.githubusercontent.com/24578097/147881907-a765ede6-c8e5-44ff-9845-db53f0634f07.png) 4 | //! 5 | //! It includes all the PASETO and custom claims but allows you to create different default claims in your custom builder and parser or use a different time crate or make up your own default business rules. As with the batteries_included layer, parsed tokens get returned as a serder_json Value. Again, specify the version and purpose to include in the crypto core: 6 | //! 7 | //! 8 | //! ```toml 9 | //! ## Includes only v4 modern sodium cipher crypto core and local (symmetric) 10 | //! ## key types with all claims and default business rules. 11 | //! 12 | //! rusty_paseto = {version = "latest", features = ["generic", "v4_local"] } 13 | //! ``` 14 | //! ``` 15 | //! # #[cfg(feature = "default")] 16 | //! # { 17 | //! // at the top of your source file 18 | //! use rusty_paseto::generic::*; 19 | //! # } 20 | //! ``` 21 | //! # Registered Claims 22 | 23 | //! Refer to the [PASETO specification](https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md) to review reserved claims for use within PASETO. 24 | mod builders; 25 | mod claims; 26 | mod parsers; 27 | 28 | pub use crate::generic::claims::*; 29 | 30 | pub use crate::core::*; 31 | 32 | pub use crate::generic::builders::*; 33 | 34 | pub use crate::generic::parsers::*; 35 | -------------------------------------------------------------------------------- /src/generic/parsers/error.rs: -------------------------------------------------------------------------------- 1 | use crate::generic::claims::PasetoClaimError; 2 | use thiserror::Error; 3 | 4 | /// Errors raised by the generic parser when validating claims or parsing a PASETO token. 5 | #[derive(Debug, Error)] 6 | pub enum GenericParserError { 7 | /// An error from the existence or non-existence or validation of a claim 8 | #[error(transparent)] 9 | ClaimError { 10 | #[from] 11 | source: PasetoClaimError, 12 | }, 13 | /// An error decrypting or validating a token 14 | #[error("A paseto cipher error occurred")] 15 | CipherError { 16 | #[from] 17 | source: crate::core::PasetoError, 18 | }, 19 | /// A JSON deserialization error for the token payload 20 | #[error("The payload was unable to be serialized into json")] 21 | PayloadJsonError { 22 | #[from] 23 | source: serde_json::Error, 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /src/generic/parsers/mod.rs: -------------------------------------------------------------------------------- 1 | mod error; 2 | mod generic_parser; 3 | 4 | pub use error::GenericParserError; 5 | pub use generic_parser::GenericParser; 6 | -------------------------------------------------------------------------------- /src/prelude/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Errors from validating claims in a parsed token 4 | #[derive(Debug, Error)] 5 | pub enum GeneralPasetoError { 6 | ///A general, unspecified paseto error 7 | #[error("A general paseto error occurred")] 8 | PasetoError(Box), 9 | #[error("An infallible error occurred")] 10 | Infallible { 11 | ///An infallible error 12 | #[from] 13 | source: std::convert::Infallible, 14 | }, 15 | ///An error with the data format 16 | #[error(transparent)] 17 | RFC3339Date(#[from] time::error::Format), 18 | } 19 | -------------------------------------------------------------------------------- /src/prelude/mod.rs: -------------------------------------------------------------------------------- 1 | //! The outermost architectural layer is called batteries_included. This layer is implemented in the [prelude](self) module. This is what most people will need. 2 | //! This feature includes JWT style claims and business rules for your PASETO token (default, but customizable expiration, issued at, not-before times, etc as described in the usage documentation and examples). 3 | //! 4 | //! ![paseto_batteries_included_small](https://user-images.githubusercontent.com/24578097/147881895-36878b22-bf17-49e4-98d7-f94920353368.png) 5 | //! 6 | //! You must specify a version and purpose with this feature in order to reduce the size of your dependencies like in the following Cargo.toml entry which only includes the V4 - Local types with batteries_included functionality: 7 | //! 8 | //! ```toml 9 | //! ## Includes only v4 modern sodium cipher crypto core and local (symmetric) 10 | //! ## key types with all claims and default business rules. 11 | //! 12 | //! rusty_paseto = {version = "latest", features = ["batteries_included", "v4_local"] } 13 | //! ``` 14 | //! ![paseto_batteries_included_v4_local_small](https://user-images.githubusercontent.com/24578097/147882822-46dac1d1-a922-4301-be45-d3341dabfee1.png) 15 | //! 16 | //! #### Feature gates 17 | //! Valid version/purpose feature combinations are as follows: 18 | //! - "v1_local" (NIST Original Symmetric Encryption) 19 | //! - "v2_local" (Sodium Original Symmetric Encryption) 20 | //! - "v3_local" (NIST Modern Symmetric Encryption) 21 | //! - "v4_local" (Sodium Modern Symmetric Encryption) 22 | //! - "v1_public" (NIST Original Asymmetric Authentication) 23 | //! - "v2_public" (Sodium Original Asymmetric Authentication) 24 | //! - "v3_public" (NIST Modern Asymmetric Authentication) 25 | //! - "v4_public" (Sodium Modern Asymmetric Authentication) 26 | 27 | mod error; 28 | mod paseto_builder; 29 | mod paseto_parser; 30 | 31 | pub use crate::generic::*; 32 | pub use error::GeneralPasetoError; 33 | pub use paseto_builder::PasetoBuilder; 34 | pub use paseto_parser::PasetoParser; 35 | -------------------------------------------------------------------------------- /tests/build_payload_from_claims_prop_test.proptest-regressions: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc fcb4492f76b8f1e307be0b2be5e8d5fd157cbb850ad73a9b73af634f3fdf843a # shrinks to claims = {"A": Object {"A": Number(5.155313317789283e132)}} 8 | -------------------------------------------------------------------------------- /tests/build_payload_from_claims_prop_test.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | * Property Tests for build_payload_from_claims Function 3 | * 4 | * This file contains property tests designed to validate the correctness and robustness 5 | * of the `build_payload_from_claims` function. The `build_payload_from_claims` function 6 | * is responsible for constructing JSON payloads from claims, ensuring they are serialized 7 | * and wrapped correctly. 8 | * 9 | * The primary goals of these tests are: 10 | * 1. **Validation**: Ensure the `build_payload_from_claims` function correctly handles different types of claims. 11 | * 2. **Robustness**: Identify and address potential edge cases that may not be covered by unit tests. 12 | * 3. **Consistency**: Verify that the function maintains the expected structure and behavior for all inputs. 13 | * 14 | * ## Test Strategy 15 | * 16 | * The property tests leverage the `proptest` crate to generate a wide range of claims, 17 | * including nested structures. The generated claims are then passed to the `build_payload_from_claims` 18 | * function, and the resulting payloads are compared against the expected outcomes. 19 | * 20 | * ## Key Test Scenarios 21 | * 22 | * - **Null Values**: Ensure null values remain null and are not wrapped unnecessarily. 23 | * - **Empty Objects**: Verify that empty maps are wrapped as empty JSON objects. 24 | * - **Primitive Values**: Confirm that primitive values (e.g., strings, numbers) remain unchanged. 25 | * - **Arrays**: Ensure arrays, including empty arrays, are wrapped correctly and consistently. 26 | * - **Nested Structures**: Validate the recursive wrapping and serialization of nested JSON objects and arrays. 27 | * 28 | * ## Findings 29 | * 30 | * - The `build_payload_from_claims` function correctly handles most input values and passes the associated unit tests. 31 | * - A specific floating-point corner case was identified during the testing process. This case involves minor 32 | * discrepancies in floating-point precision, which is a common issue in many systems. The identified corner 33 | * case has been documented and is not critical for most practical use cases. 34 | * 35 | * ## Conclusion 36 | * 37 | * The property tests demonstrate that the `build_payload_from_claims` function is robust and reliable for most practical 38 | * use cases. While a specific floating-point corner case remains, the function's behavior is consistent with the expected 39 | * outcomes for a wide range of input values. 40 | * 41 | * To run these tests, use the following command: 42 | * 43 | * ```sh 44 | * cargo test -- --ignored 45 | * ``` 46 | * 47 | * This approach ensures comprehensive validation of the `build_payload_from_claims` function, contributing to the overall 48 | * stability and reliability of the system. 49 | */ 50 | 51 | use std::collections::HashMap; 52 | 53 | use proptest::prelude::*; 54 | use erased_serde::Serialize; 55 | use serde_json::{Map, Number, Value}; 56 | 57 | // Define a strategy to generate arbitrary JSON values 58 | fn arb_json() -> impl Strategy { 59 | let leaf = prop_oneof![ 60 | Just(Json::Null), 61 | any::().prop_map(Json::Bool), 62 | any::().prop_map(Json::Number), 63 | "[a-zA-Z0-9_]+".prop_map(Json::String), 64 | ]; 65 | leaf.prop_recursive( 66 | 3, // 3 levels deep 67 | 64, // Shoot for maximum size of 64 nodes 68 | 10, // We put up to 10 items per collection 69 | |inner| prop_oneof![ 70 | prop::collection::vec(inner.clone(), 0..10).prop_map(Json::Array), 71 | prop::collection::hash_map("[a-zA-Z_][a-zA-Z0-9_]*", inner, 0..10).prop_map(Json::Map), 72 | ], 73 | ) 74 | } 75 | 76 | #[derive(Clone, Debug)] 77 | enum Json { 78 | Null, 79 | Bool(bool), 80 | Number(f64), 81 | String(String), 82 | Array(Vec), 83 | Map(HashMap), 84 | } 85 | 86 | // Convert our custom Json enum to serde_json::Value 87 | impl From for Value { 88 | fn from(json: Json) -> Self { 89 | match json { 90 | Json::Null => Value::Null, 91 | Json::Bool(b) => Value::Bool(b), 92 | Json::Number(n) => Value::Number(Number::from_f64(n).unwrap()), 93 | Json::String(s) => Value::String(s), 94 | Json::Array(arr) => Value::Array(arr.into_iter().map(Value::from).collect()), 95 | Json::Map(map) => Value::Object(map.into_iter().map(|(k, v)| (k, Value::from(v))).collect()), 96 | } 97 | } 98 | } 99 | 100 | 101 | // Wrap claims in an outer JSON object to ensure proper nesting 102 | fn wrap_claims(claims: HashMap) -> Value { 103 | let wrapped: HashMap = claims 104 | .into_iter() 105 | .map(|(k, v)| (k, wrap_value(v))) 106 | .collect(); 107 | Value::Object(Map::from_iter(wrapped)) 108 | } 109 | 110 | // Recursively wrap values to ensure all values are valid JSON objects 111 | fn wrap_value(value: Value) -> Value { 112 | match value { 113 | Value::Object(map) => { 114 | if map.is_empty() { 115 | Value::Object(Map::new()) // Ensure empty map is wrapped as an empty object 116 | } else { 117 | Value::Object(map.into_iter().map(|(k, v)| (k, wrap_value(v))).collect()) 118 | } 119 | } 120 | Value::Array(arr) => Value::Array(arr.into_iter().map(wrap_value).collect()), 121 | Value::Null => Value::Null, // Do not wrap null values 122 | other => Value::Object(Map::from_iter(vec![("value".to_string(), other)])), // Wrap primitive values 123 | } 124 | } 125 | 126 | // Define a strategy to generate arbitrary claims with valid JSON string keys 127 | fn claim_strategy() -> impl Strategy> { 128 | prop::collection::hash_map("[a-zA-Z_][a-zA-Z0-9_]*", arb_json().prop_map(Value::from), 1..10) 129 | } 130 | 131 | // Simulated GenericBuilder structure 132 | struct SimulatedGenericBuilder { 133 | claims: HashMap>, 134 | } 135 | 136 | impl SimulatedGenericBuilder { 137 | pub fn new() -> Self { 138 | Self { 139 | claims: HashMap::new(), 140 | } 141 | } 142 | 143 | pub fn extend_claims(&mut self, claims: HashMap>) { 144 | self.claims.extend(claims); 145 | } 146 | 147 | pub fn build_payload_from_claims(&mut self) -> Result { 148 | let claims = std::mem::take(&mut self.claims); 149 | let serialized_claims: HashMap = claims 150 | .into_iter() 151 | .map(|(k, v)| (k, serde_json::to_value(v).unwrap_or(Value::Null))) 152 | .collect(); 153 | let wrapped_claims = wrap_claims(serialized_claims); 154 | serde_json::to_string(&wrapped_claims) 155 | } 156 | } 157 | 158 | // Custom function to compare JSON values with tolerance for floating-point numbers 159 | fn compare_json_values(a: &Value, b: &Value) -> bool { 160 | match (a, b) { 161 | (Value::Number(a_num), Value::Number(b_num)) => { 162 | let a_f64 = a_num.as_f64().unwrap(); 163 | let b_f64 = b_num.as_f64().unwrap(); 164 | (a_f64 - b_f64).abs() < 1e-10 // Tolerance for floating-point comparison 165 | } 166 | (Value::Object(a_map), Value::Object(b_map)) => { 167 | if a_map.len() != b_map.len() { 168 | return false; 169 | } 170 | for (key, a_value) in a_map { 171 | if let Some(b_value) = b_map.get(key) { 172 | if !compare_json_values(a_value, b_value) { 173 | return false; 174 | } 175 | } else { 176 | return false; 177 | } 178 | } 179 | true 180 | } 181 | (Value::Array(a_arr), Value::Array(b_arr)) => { 182 | if a_arr.len() != b_arr.len() { 183 | return false; 184 | } 185 | for (a_value, b_value) in a_arr.iter().zip(b_arr.iter()) { 186 | if !compare_json_values(a_value, b_value) { 187 | return false; 188 | } 189 | } 190 | true 191 | } 192 | _ => a == b, 193 | } 194 | } 195 | 196 | proptest! { 197 | #[test] 198 | #[ignore] 199 | fn test_build_payload_from_claims(claims in claim_strategy()) { 200 | // Debug print to check the generated claims 201 | println!("Generated claims: {:?}", claims); 202 | let wrapped_claims = wrap_claims(claims.clone()); 203 | println!("Wrapped claims: {:?}", wrapped_claims); 204 | 205 | let mut builder = SimulatedGenericBuilder::new(); 206 | builder.extend_claims(claims.clone().into_iter().map(|(k, v)| (k, Box::new(v) as Box)).collect()); 207 | 208 | let payload_result = builder.build_payload_from_claims(); 209 | // Check if payload is built successfully 210 | prop_assert!(payload_result.is_ok(), "Failed to build payload: {:?}", payload_result); 211 | 212 | let payload = payload_result.unwrap(); 213 | println!("Generated payload: {}", payload); 214 | let payload_value: Value = serde_json::from_str(&payload).expect("Payload should be valid JSON"); 215 | 216 | // Check if all claims are present in the payload 217 | for (key, _) in claims { 218 | let expected_value = wrapped_claims.get(&key).unwrap(); 219 | let actual_value = payload_value.get(&key).unwrap(); 220 | prop_assert!(compare_json_values(expected_value, actual_value), "Key '{}' not found or value mismatch: expected {:?}, got {:?}", key, expected_value, actual_value); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/generic_claims_wrap_value_prop_test.proptest-regressions: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc d900fc433a21df07d9701323e3c92c90b64e205d311663fa6385d19609329102 # shrinks to input = Map({}) 8 | cc 96f261d316383697b99b1fb42233ad10c27f812b3fd2f3c2b0337ef3ee85e246 # shrinks to input = Array([]) 9 | cc 6903a0c80fa97eb92ccdd54c8087b1b96ce86dfc3d429e4965f20c2a892c058d # shrinks to input = Bool(false) 10 | -------------------------------------------------------------------------------- /tests/generic_claims_wrap_value_prop_test.rs: -------------------------------------------------------------------------------- 1 | /*! 2 | * Property Tests for wrap_value Function 3 | * 4 | * This file contains property tests designed to validate the correctness and robustness 5 | * of the `wrap_value` function. The `wrap_value` function ensures that all values are 6 | * recursively wrapped to maintain valid JSON objects, handling various edge cases 7 | * including empty objects, arrays, and null values. 8 | * 9 | * The primary goals of these tests are: 10 | * 1. **Validation**: Ensure the `wrap_value` function correctly handles different types of JSON values. 11 | * 2. **Robustness**: Identify and address potential edge cases that may not be covered by unit tests. 12 | * 3. **Consistency**: Verify that the function maintains the expected structure and behavior for all inputs. 13 | * 14 | * ## Test Strategy 15 | * 16 | * The property tests leverage the `proptest` crate to generate a wide range of JSON values, 17 | * including nested structures. The generated values are then passed to the `wrap_value` 18 | * function, and the resulting wrapped values are compared against the expected outcomes. 19 | * 20 | * ## Key Test Scenarios 21 | * 22 | * - **Null Values**: Ensure null values remain null and are not wrapped unnecessarily. 23 | * - **Empty Objects**: Verify that empty maps are wrapped as empty JSON objects. 24 | * - **Primitive Values**: Confirm that primitive values (e.g., strings, numbers) remain unchanged. 25 | * - **Arrays**: Ensure arrays, including empty arrays, are wrapped correctly and consistently. 26 | * - **Nested Structures**: Validate the recursive wrapping of nested JSON objects and arrays. 27 | * 28 | * ## Findings 29 | * 30 | * - The `wrap_value` function correctly handles a wide range of input values, passing all property tests. 31 | * - No significant corner cases were identified during the testing process, indicating that the function 32 | * is robust and reliable for most practical use cases. 33 | * 34 | * ## Conclusion 35 | * 36 | * The property tests demonstrate that the `wrap_value` function is robust and reliable for most practical 37 | * use cases. The function's behavior is consistent with the expected outcomes for a wide range of input values. 38 | * 39 | * This approach ensures comprehensive validation of the `wrap_value` function, contributing to the overall 40 | * stability and reliability of the system. 41 | */ 42 | 43 | use std::collections::HashMap; 44 | 45 | use proptest::prelude::*; 46 | use serde_json::{Map, Value}; 47 | 48 | // Define a strategy to generate arbitrary JSON values 49 | fn arb_json() -> impl Strategy { 50 | let leaf = prop_oneof![ 51 | Just(Json::Null), 52 | any::().prop_map(Json::Bool), 53 | any::().prop_map(Json::Number), 54 | "[a-zA-Z0-9_]+".prop_map(Json::String), 55 | ]; 56 | leaf.prop_recursive( 57 | 3, // 3 levels deep 58 | 64, // Shoot for maximum size of 64 nodes 59 | 10, // We put up to 10 items per collection 60 | |inner| prop_oneof![ 61 | prop::collection::vec(inner.clone(), 0..10).prop_map(Json::Array), 62 | prop::collection::hash_map("[a-zA-Z_][a-zA-Z0-9_]*", inner, 0..10).prop_map(Json::Map), 63 | ], 64 | ) 65 | } 66 | 67 | #[derive(Clone, Debug)] 68 | enum Json { 69 | Null, 70 | Bool(bool), 71 | Number(f64), 72 | String(String), 73 | Array(Vec), 74 | Map(HashMap), 75 | } 76 | 77 | // Convert our custom Json enum to serde_json::Value 78 | impl From for Value { 79 | fn from(json: Json) -> Self { 80 | match json { 81 | Json::Null => Value::Null, 82 | Json::Bool(b) => Value::Bool(b), 83 | Json::Number(n) => Value::Number(serde_json::Number::from_f64(n).unwrap()), 84 | Json::String(s) => Value::String(s), 85 | Json::Array(arr) => Value::Array(arr.into_iter().map(Value::from).collect()), 86 | Json::Map(map) => Value::Object(map.into_iter().map(|(k, v)| (k, Value::from(v))).collect()), 87 | } 88 | } 89 | } 90 | 91 | fn wrap_value(value: Value) -> Value { 92 | match value { 93 | Value::Object(map) => { 94 | if map.is_empty() { 95 | Value::Object(Map::new()) // Ensure empty map is wrapped as an empty object 96 | } else { 97 | Value::Object(map.into_iter().map(|(k, v)| (k, wrap_value(v))).collect()) 98 | } 99 | } 100 | Value::Array(arr) => Value::Array(arr.into_iter().map(wrap_value).collect()), 101 | Value::Null => Value::Null, // Do not wrap null values 102 | other => other, // Do not wrap primitive values 103 | } 104 | } 105 | proptest! { 106 | #[test] 107 | fn test_wrap_value(input in arb_json()) { 108 | let value: Value = input.into(); 109 | let wrapped_value = wrap_value(value.clone()); 110 | 111 | // Ensure null values remain null 112 | if let Value::Null = value { 113 | prop_assert_eq!(wrapped_value, Value::Null); 114 | } else if let Value::Object(map) = &value { 115 | if map.is_empty() { 116 | prop_assert_eq!(wrapped_value, Value::Object(Map::new())); 117 | } else { 118 | // For non-empty maps, ensure they are wrapped correctly 119 | for (k, v) in map { 120 | let wrapped_sub_value = wrapped_value.get(k).expect("Key should exist in wrapped map"); 121 | let expected_sub_value = wrap_value(v.clone()); 122 | prop_assert_eq!(wrapped_sub_value, &expected_sub_value, "Key '{}' not wrapped correctly", k); 123 | } 124 | } 125 | } else if let Value::Array(arr) = &value { 126 | // Ensure arrays are wrapped correctly, including empty arrays 127 | for (original, wrapped) in arr.iter().zip(wrapped_value.as_array().expect("Wrapped value should be an array")) { 128 | let expected_sub_value = wrap_value(original.clone()); 129 | prop_assert_eq!(wrapped, &expected_sub_value, "Array element not wrapped correctly"); 130 | } 131 | if arr.is_empty() { 132 | prop_assert_eq!(wrapped_value, Value::Array(vec![])); 133 | } 134 | } else { 135 | // For other values, ensure they remain unchanged 136 | prop_assert_eq!(wrapped_value, value); 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /tests/v1_public_test_vectors_private_key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/tests/v1_public_test_vectors_private_key.der -------------------------------------------------------------------------------- /tests/v1_public_test_vectors_private_key.pk8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/tests/v1_public_test_vectors_private_key.pk8 -------------------------------------------------------------------------------- /tests/v1_public_test_vectors_public_key.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rrrodzilla/rusty_paseto/55899e08993ec0b3c6e243d8579ddb1be841cd8e/tests/v1_public_test_vectors_public_key.der -------------------------------------------------------------------------------- /tests/version1_test_vectors.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(test, feature = "v1"))] 2 | mod v1_test_vectors { 3 | use anyhow::Result; 4 | use rusty_paseto::core::*; 5 | use serde_json::json; 6 | 7 | #[cfg(feature = "v1_local")] 8 | #[test] 9 | fn test_1_e_1() -> Result<()> { 10 | //setup 11 | //let key = Key::<32>::try_from("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f")?; 12 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 13 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | )?); 15 | let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 16 | let nonce = PasetoNonce::::from(&nonce); 17 | 18 | let payload = json!({"data": "this is a signed message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 19 | let payload = payload.as_str(); 20 | let payload = Payload::from(payload); 21 | 22 | // // //create a public V1 token 23 | let token = Paseto::::builder() 24 | .set_payload(payload) 25 | .try_encrypt(&key, &nonce)?; 26 | 27 | // //validate the test vector 28 | assert_eq!(token.to_string(), "v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8"); 29 | 30 | ////now let's try to decrypt it 31 | let json = Paseto::::try_decrypt(&token, &key, None)?; 32 | assert_eq!(payload, json); 33 | Ok(()) 34 | } 35 | 36 | #[cfg(feature = "v1_local")] 37 | #[test] 38 | fn test_1_e_2() -> Result<()> { 39 | //setup 40 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 41 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 42 | )?); 43 | let nonce = Key::<32>::try_from("0000000000000000000000000000000000000000000000000000000000000000")?; 44 | let nonce = PasetoNonce::::from(&nonce); 45 | 46 | let payload = json!({"data": "this is a secret message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 47 | let payload = payload.as_str(); 48 | let payload = Payload::from(payload); 49 | 50 | // // //create a public V1 token 51 | let token = Paseto::::builder() 52 | .set_payload(payload) 53 | .try_encrypt(&key, &nonce)?; 54 | 55 | // //validate the test vector 56 | assert_eq!(token.to_string(), "v1.local.w_NOpjgte4bX-2i1JAiTQzHoGUVOgc2yqKqsnYGmaPaCu_KWUkRGlCRnOvZZxeH4HTykY7AE_jkzSXAYBkQ1QnwvKS16uTXNfnmp8IRknY76I2m3S5qsM8klxWQQKFDuQHl8xXV0MwAoeFh9X6vbwIqrLlof3s4PMjRDwKsxYzkMr1RvfDI8emoPoW83q4Q60_xpHaw"); 57 | 58 | ////now let's try to decrypt it 59 | let json = Paseto::::try_decrypt(&token, &key, None)?; 60 | assert_eq!(payload, json); 61 | Ok(()) 62 | } 63 | 64 | #[cfg(feature = "v1_local")] 65 | #[test] 66 | fn test_1_e_3() -> Result<()> { 67 | //setup 68 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 69 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 70 | )?); 71 | let nonce = Key::<32>::try_from("26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2")?; 72 | let nonce = PasetoNonce::::from(&nonce); 73 | 74 | let payload = json!({"data": "this is a signed message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 75 | let payload = payload.as_str(); 76 | let payload = Payload::from(payload); 77 | 78 | // // //create a public V1 token 79 | let token = Paseto::::builder() 80 | .set_payload(payload) 81 | .try_encrypt(&key, &nonce)?; 82 | 83 | // //validate the test vector 84 | assert_eq!(token.to_string(), "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3syBYyjKIOeWnsFQB6Yef-1ov9rvqt7TmwONUHeJUYk4IK_JEdUeo_uFRqAIgHsiGCg"); 85 | 86 | ////now let's try to decrypt it 87 | let json = Paseto::::try_decrypt(&token, &key, None)?; 88 | assert_eq!(payload, json); 89 | Ok(()) 90 | } 91 | 92 | #[cfg(feature = "v1_local")] 93 | #[test] 94 | fn test_1_e_4() -> Result<()> { 95 | //setup 96 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 97 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 98 | )?); 99 | let nonce = Key::<32>::try_from("26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2")?; 100 | let nonce = PasetoNonce::::from(&nonce); 101 | 102 | let payload = json!({"data": "this is a secret message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 103 | let payload = payload.as_str(); 104 | let payload = Payload::from(payload); 105 | 106 | //let footer = Footer::from("{\"kid\":\"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN\"}"); 107 | 108 | // // //create a public V1 token 109 | let token = Paseto::::builder() 110 | .set_payload(payload) 111 | .try_encrypt(&key, &nonce)?; 112 | 113 | // //validate the test vector 114 | assert_eq!(token.to_string(), "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvejdt2Srz_5Q0QG4oiz1gB_wmv4U5pifedaZbHXUTWXchFEi0etJ4u6tqgxZSklcec"); 115 | 116 | ////now let's try to decrypt it 117 | let json = Paseto::::try_decrypt(&token, &key, None)?; 118 | assert_eq!(payload, json); 119 | Ok(()) 120 | } 121 | 122 | #[cfg(feature = "v1_local")] 123 | #[test] 124 | fn test_1_e_5() -> Result<()> { 125 | //setup 126 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 127 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 128 | )?); 129 | let nonce = Key::<32>::try_from("26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2")?; 130 | let nonce = PasetoNonce::::from(&nonce); 131 | 132 | let payload = json!({"data": "this is a signed message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 133 | let payload = payload.as_str(); 134 | let payload = Payload::from(payload); 135 | 136 | let footer = json!({"kid":"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo"}).to_string(); 137 | let footer = footer.as_str(); 138 | let footer = Footer::from(footer); 139 | // // //create a public V1 token 140 | let token = Paseto::::builder() 141 | .set_payload(payload) 142 | .set_footer(footer) 143 | .try_encrypt(&key, &nonce)?; 144 | 145 | // //validate the test vector 146 | assert_eq!(token.to_string(), "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3szA28OabR192eRqiyspQ6xPM35NMR-04-FhRJZEWiF0W5oWjPVtGPjeVjm2DI4YtJg.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9"); 147 | 148 | ////now let's try to decrypt it 149 | let json = Paseto::::try_decrypt(&token, &key, footer)?; 150 | assert_eq!(payload, json); 151 | Ok(()) 152 | } 153 | 154 | #[cfg(feature = "v1_local")] 155 | #[test] 156 | fn test_1_e_6() -> Result<()> { 157 | //setup 158 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 159 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 160 | )?); 161 | let nonce = Key::<32>::try_from("26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2")?; 162 | let nonce = PasetoNonce::::from(&nonce); 163 | 164 | let payload = json!({"data": "this is a secret message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 165 | let payload = payload.as_str(); 166 | let payload = Payload::from(payload); 167 | 168 | let footer = json!({"kid":"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo"}).to_string(); 169 | let footer = footer.as_str(); 170 | let footer = Footer::from(footer); 171 | // // //create a public V1 token 172 | let token = Paseto::::builder() 173 | .set_payload(payload) 174 | .set_footer(footer) 175 | .try_encrypt(&key, &nonce)?; 176 | 177 | // //validate the test vector 178 | assert_eq!(token.to_string(), "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvcT2vnER6NrJ7xIowvFba6J4qMlFhBnYSxHEq9v9NlzcKsz1zscdjcAiXnEuCHyRSc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9"); 179 | 180 | ////now let's try to decrypt it 181 | let json = Paseto::::try_decrypt(&token, &key, footer)?; 182 | assert_eq!(payload, json); 183 | Ok(()) 184 | } 185 | 186 | #[cfg(feature = "v1_local")] 187 | #[test] 188 | fn test_1_e_7() -> Result<()> { 189 | //setup 190 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 191 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 192 | )?); 193 | let nonce = Key::<32>::try_from("26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2")?; 194 | let nonce = PasetoNonce::::from(&nonce); 195 | 196 | let payload = json!({"data": "this is a signed message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 197 | let payload = payload.as_str(); 198 | let payload = Payload::from(payload); 199 | 200 | let footer = json!({"kid":"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo"}).to_string(); 201 | let footer = footer.as_str(); 202 | let footer = Footer::from(footer); 203 | 204 | // // //create a public V1 token 205 | let token = Paseto::::builder() 206 | .set_payload(payload) 207 | .set_footer(footer) 208 | .try_encrypt(&key, &nonce)?; 209 | 210 | // //validate the test vector 211 | assert_eq!(token.to_string(), "v1.local.4VyfcVcFAOAbB8yEM1j1Ob7Iez5VZJy5kHNsQxmlrAwKUbOtq9cv39T2fC0MDWafX0nQJ4grFZzTdroMvU772RW-X1oTtoFBjsl_3YYHWnwgqzs0aFc3ejjORmKP4KUM339W3szA28OabR192eRqiyspQ6xPM35NMR-04-FhRJZEWiF0W5oWjPVtGPjeVjm2DI4YtJg.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9"); 212 | 213 | ////now let's try to decrypt it 214 | let json = Paseto::::try_decrypt(&token, &key, footer)?; 215 | assert_eq!(payload, json); 216 | Ok(()) 217 | } 218 | 219 | #[cfg(feature = "v1_local")] 220 | #[test] 221 | fn test_1_e_8() -> Result<()> { 222 | //setup 223 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 224 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 225 | )?); 226 | let nonce = Key::<32>::try_from("26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2")?; 227 | let nonce = PasetoNonce::::from(&nonce); 228 | 229 | let payload = json!({"data": "this is a secret message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 230 | let payload = payload.as_str(); 231 | let payload = Payload::from(payload); 232 | 233 | let footer = json!({"kid":"UbkK8Y6iv4GZhFp6Tx3IWLWLfNXSEvJcdT3zdR65YZxo"}).to_string(); 234 | let footer = footer.as_str(); 235 | let footer = Footer::from(footer); 236 | 237 | // // //create a public V1 token 238 | let token = Paseto::::builder() 239 | .set_payload(payload) 240 | .set_footer(footer) 241 | .try_encrypt(&key, &nonce)?; 242 | 243 | // //validate the test vector 244 | assert_eq!(token.to_string(), "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvcT2vnER6NrJ7xIowvFba6J4qMlFhBnYSxHEq9v9NlzcKsz1zscdjcAiXnEuCHyRSc.eyJraWQiOiJVYmtLOFk2aXY0R1poRnA2VHgzSVdMV0xmTlhTRXZKY2RUM3pkUjY1WVp4byJ9"); 245 | 246 | ////now let's try to decrypt it 247 | let json = Paseto::::try_decrypt(&token, &key, footer)?; 248 | assert_eq!(payload, json); 249 | Ok(()) 250 | } 251 | 252 | #[cfg(feature = "v1_local")] 253 | #[test] 254 | fn test_1_e_9() -> Result<()> { 255 | //setup 256 | let key = PasetoSymmetricKey::::from(Key::<32>::try_from( 257 | "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 258 | )?); 259 | let nonce = Key::<32>::try_from("26f7553354482a1d91d4784627854b8da6b8042a7966523c2b404e8dbbe7f7f2")?; 260 | let nonce = PasetoNonce::::from(&nonce); 261 | 262 | let payload = json!({"data": "this is a secret message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 263 | let payload = payload.as_str(); 264 | let payload = Payload::from(payload); 265 | 266 | let footer = Footer::from("arbitrary-string-that-isn't-json"); 267 | 268 | // // //create a public V1 token 269 | let token = Paseto::::builder() 270 | .set_payload(payload) 271 | .set_footer(footer) 272 | .try_encrypt(&key, &nonce)?; 273 | 274 | // //validate the test vector 275 | assert_eq!(token.to_string(), "v1.local.IddlRQmpk6ojcD10z1EYdLexXvYiadtY0MrYQaRnq3dnqKIWcbbpOcgXdMIkm3_3gksirTj81bvWrWkQwcUHilt-tQo7LZK8I6HCK1V78B9YeEqGNeeWXOyWWHoJQIe0d5nTdvdgNpe3vI21jV2YL7WVG5p63_JxxzLckBu9azQ0GlDMdPxNAxoyvmU1wbpSbRB9Iw4.YXJiaXRyYXJ5LXN0cmluZy10aGF0LWlzbid0LWpzb24"); 276 | 277 | ////now let's try to decrypt it 278 | let json = Paseto::::try_decrypt(&token, &key, footer)?; 279 | assert_eq!(payload, json); 280 | Ok(()) 281 | } 282 | 283 | #[cfg(feature = "v1_public")] 284 | #[test] 285 | fn test_1_s_1() -> Result<()> { 286 | let private_key = include_bytes!("v1_public_test_vectors_private_key.pk8"); 287 | let pk: &[u8] = private_key; 288 | let private_key = PasetoAsymmetricPrivateKey::::from(pk); 289 | 290 | let public_key = include_bytes!("v1_public_test_vectors_public_key.der"); 291 | let pubk: &[u8] = public_key; 292 | let public_key = PasetoAsymmetricPublicKey::::from(pubk); 293 | 294 | let payload = json!({"data": "this is a signed message", "exp":"2019-01-01T00:00:00+00:00"}).to_string(); 295 | let payload = payload.as_str(); 296 | let payload = Payload::from(payload); 297 | 298 | //create a public V1 token 299 | let token = Paseto::::builder() 300 | .set_payload(payload) 301 | .try_sign(&private_key)?; 302 | 303 | //now let's try to decrypt it 304 | let json = Paseto::::try_verify(&token, &public_key, None)?; 305 | assert_eq!(payload, json); 306 | Ok(()) 307 | } 308 | 309 | #[cfg(feature = "v1_public")] 310 | #[test] 311 | fn test_1_s_2() -> Result<()> { 312 | //setup 313 | let private_key = include_bytes!("v1_public_test_vectors_private_key.pk8"); 314 | let pk: &[u8] = private_key; 315 | 316 | let private_key = PasetoAsymmetricPrivateKey::::from(pk); 317 | 318 | let public_key = include_bytes!("v1_public_test_vectors_public_key.der"); 319 | let pubk: &[u8] = public_key; 320 | let public_key = PasetoAsymmetricPublicKey::::from(pubk); 321 | 322 | let payload = json!({"data": "this is a signed message","exp": "2019-01-01T00:00:00+00:00"}).to_string(); 323 | let footer = json!({"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}).to_string(); 324 | 325 | //create message for test vector 326 | // eprintln!("\nJSON INFO: {}\n", json); 327 | let message = Payload::from(payload.as_str()); 328 | let footer = Footer::from(footer.as_str()); 329 | 330 | // // //create a local v2 token 331 | //let token = Paseto::::build_token(header, message, &key, None); 332 | let token = Paseto::::default() 333 | .set_payload(message.clone()) 334 | .set_footer(footer.clone()) 335 | .try_sign(&private_key)?; 336 | 337 | // //validate the test vector 338 | //now let's try to decrypt it 339 | let json = Paseto::::try_verify(&token, &public_key, footer)?; 340 | assert_eq!(payload, json); 341 | Ok(()) 342 | } 343 | 344 | #[cfg(feature = "v1_public")] 345 | #[test] 346 | fn test_1_s_3() -> Result<()> { 347 | //setup 348 | let private_key = include_bytes!("v1_public_test_vectors_private_key.pk8"); 349 | let pk: &[u8] = private_key; 350 | 351 | let private_key = PasetoAsymmetricPrivateKey::::from(pk); 352 | 353 | let public_key = include_bytes!("v1_public_test_vectors_public_key.der"); 354 | let pubk: &[u8] = public_key; 355 | let public_key = PasetoAsymmetricPublicKey::::from(pubk); 356 | 357 | let payload = json!({"data": "this is a signed message","exp": "2019-01-01T00:00:00+00:00"}).to_string(); 358 | let footer = json!({"kid":"zVhMiPBP9fRf2snEcT7gFTioeA9COcNy9DfgL1W60haN"}).to_string(); 359 | 360 | //create message for test vector 361 | // eprintln!("\nJSON INFO: {}\n", json); 362 | let message = Payload::from(payload.as_str()); 363 | let footer = Footer::from(footer.as_str()); 364 | 365 | // // //create a local v2 token 366 | //let token = Paseto::::build_token(header, message, &key, None); 367 | let token = Paseto::::default() 368 | .set_payload(message.clone()) 369 | .set_footer(footer.clone()) 370 | .try_sign(&private_key)?; 371 | 372 | //now let's try to decrypt it 373 | let json = Paseto::::try_verify(&token, &public_key, footer)?; 374 | assert_eq!(payload, json); 375 | Ok(()) 376 | } 377 | 378 | #[cfg(feature = "v1_public")] 379 | #[test] 380 | #[should_panic] 381 | fn test_1_f_1() { 382 | //this test is prevented at compile time and passes by defacto 383 | panic!("non-compileable test") 384 | } 385 | 386 | #[cfg(feature = "v1_public")] 387 | #[test] 388 | #[should_panic] 389 | fn test_1_f_2() { 390 | //this test is prevented at compile time and passes by defacto 391 | panic!("non-compileable test") 392 | } 393 | } 394 | --------------------------------------------------------------------------------