Repository: instructure/paseto
Branch: trunk
Commit: bea8f7923f82
Files: 28
Total size: 95.9 KB
Directory structure:
gitextract_y1ntokrc/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── dependabot.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE
├── README.md
├── examples/
│ ├── direct-protocol.rs
│ ├── local-using-builders.rs
│ └── public-using-builder.rs
├── rustfmt.toml
└── src/
├── errors.rs
├── lib.rs
├── pae.rs
├── tokens/
│ ├── builder.rs
│ └── mod.rs
├── v1/
│ ├── local.rs
│ ├── mod.rs
│ ├── public.rs
│ ├── signature_rsa_example_private_key.der
│ └── signature_rsa_example_public_key.der
└── v2/
├── local.rs
├── mod.rs
└── public.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Run '...'
2. See output '...'
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Versions (please complete the following information):**
- OS: [e.g. iOS 12]
- Rustc version [rustc --version]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: "[FR] New"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE.md
================================================
Please read the following carefully before opening a new issue.
Your issue may be closed if it does not provide the information required by this template.
- If you have a question about how to use paseto; please start the issue title with: [QUESTION]
- If you have a feature request, please statr the issue title with: [FR]
- Make sure your issue reproduces on the latest version!
--- Delete everything above this line ---
### Description ###
Explain what you did, what you expected to happen, and what actually happens.
### Additional Information ###
* Rust Version [FILL THIS OUT: Can be grabbed with: `rustc --version` on your CLI.]
* Platform: [FILL THIS OUT: Windows, Mac, or Linux? Which Version?]
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!--
Thanks for submitting a PR! We want to make contributing to Paseto as easy as possible.
Please read these instructions carefully:
- [ ] Explain the **motivation** for making this change.
- [ ] Provide a **test plan** demonstrating that the code is solid.
- [ ] Match the **code formatting** of the rest of the codebase.
- [ ] Make sure to **add tests** to help keep code coverage up.
-->
## Motivation <!-- (required) --> ##
What existing problem does the pull request solve?
## Test Plan <!-- (required) --> ##
A good test plan has the exact commands you ran and their output.
If you have added code that should be tested, add tests.
<!--
## Next Steps ##
- Small pull requests are much easier to review and more likely to get merged. Make sure the PR does only one thing, otherwise please split it.
- Make sure all **tests pass**, we will run automated tests, but you can run it yourself by running `cargo test`.
-->
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: cargo
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
================================================
FILE: .gitignore
================================================
/target/
**/*.rs.bk
Cargo.lock
================================================
FILE: CHANGELOG.md
================================================
## <Unreleased>
* documentation overhaul
* move to thiserror over failure
* allow setting multiple claims at once in the builder
* time has been upgraded to 0.3
* chachapoly1305 has been upgraded to 0.9.0
## 2.0.2+1.0.3
* Migrate away from sodiumoxide as it has become less, and less maintained not
providing a stable secure base anymore. Migrate to supported rust-crypto
projects. `SodiumErrors` will keep it's name, but now represents these
replacements for "sodium".
## 2.0.1+1.0.3
* Fix mistake where token builder would incorrectly force `nbf`/`iat`/`exp` fields.
## 2.0.0+1.0.3
* Change `pae::pae` to borrow a slice of slices (`&[&[u8]]`) instead of taking ownership of a `Vec<Vec<u8>>`.
* High-level functions like `validate_local_token` and `validate_public_token` now take the `key` by reference.
* The reference to `key` passed as argument to `v1::public::public_paseto` is not longer taken as mutable.
* `tokens::PasetoBuilder` methods have been changed to only take references
* Support for the time crate has been added with a feature, this should not be used in conjunction with chrono.
* Create better error messages to hopefully be less user hostile.
* update dependencies.
* versions now have a "build number" to indicate what upstream version they track.
## 1.0.7
* Remove `mut` from the keys used by v2/local.rs.
* Switch to taking messages, footers, and keys by reference.
## 1.0.6
* Use newer github actions.
* Remove Azure Pipelines.
* Allow TokenBuilder to just need a public key for validation.
* Make JSON Payload Validation public so anyone can use it.
## 1.0.5
* Upgrade Ring
## 1.0.4
* Start running CI on Macs.
* Start running CI on nightly/beta builds.
* Start running CI every night.
* Upgrade openssl.
* Upgrade Ring.
* Remove direct dependency on untrusted.
## 1.0.3
* Bump Dependencies to latest version.
* Sodiumoxide segfault on mac-os-x has been fixed.
## 1.0.2
* Bump Dependencies to latest versions.
* Use sodiumoxide over libsodium_ffi directly. (no more unsafe \o/)
* Update to rust 2018 epoch.
* Can no longer export `SODIUM_BUILD_STATIC`, and `SODIUM_STATIC`
## 1.0.1
* Bump Dependencies to latest versions.
* Remove some unneeded crypto crates to lower total surface area of attack.
* Allow V2 Public to just accept public key bytes.
* Ensure all tests still pass.
## 0.5.0
* Initial Public Release
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to Paseto #
We want to make contributing to this project as easy and transparent as
possible.
## Pull Requests ##
We actively welcome your pull requests.
1. Fork the repo and create your branch from `trunk`.
2. Make sure your code builds.
3. Make sure you update documentation where necessary.
4. Make sure you add tests where also necessary, and that they pass.
5. Please run rustfmt on all code before sending in a PR (and ensure our rustfmt.toml gets picked up so it uses tabs).
## Issues ##
We use GitHub issues to track public bugs. Please ensure your description is
clear and has sufficient instructions to be able to reproduce the issue.
## License ##
By contributing to Paseto, you agree that your contributions will be licensed
under its MIT license.
================================================
FILE: Cargo.toml
================================================
[package]
name = "paseto"
description = "An alternative token format to JWT"
version = "3.0.0+1.0.3"
repository = "https://github.com/instructure/paseto"
license = "MIT"
authors = [
"Cynthia Coan <cynthia@coan.dev>"
]
edition = "2018"
[features]
default = ["v1", "v2", "easy_tokens_chrono"]
v1 = ["openssl"]
v2 = ["blake2", "chacha20poly1305"]
easy_tokens_chrono = ["serde_json", "chrono"]
easy_tokens_time = ["serde_json", "time"]
[dependencies]
base64 = "^0.13"
blake2 = { version = "^0.9.2", optional = true }
chacha20poly1305 = { version = "^0.9.0", optional = true }
chrono = { version = "^0.4", optional = true, features = ["serde"] }
openssl = { version = "~0.10.36", optional = true }
ring = { version = "^0.16", features = ["std"] }
serde_json = { version = "^1.0.68", optional = true }
time = { version = "^0.3", optional = true, features = ["serde-human-readable"] }
thiserror = "^1.0.29"
[dev-dependencies]
hex = "^0.4.3"
================================================
FILE: LICENSE
================================================
Copyright 2018 Instructure
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: README.md
================================================
# Paseto Rust #
Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of the [many design deficits that plague the JOSE standards][blog_post].
This is directly adapted from the reference implemenation made by paragon-ie, which can be found: [HERE][reference_impl].
_NOTE: The license of the original paseto implementation is ISC which is functionally equivelant to MIT, but located: [HERE][reference_license]_
## What is Paseto? ##
Paseto (Platform-Agnostic SEcurity TOkens) is a specification and reference implementation for secure stateless tokens. You
can find a lot of info about the motivation + benefits of using paseto inside the original paseto repo: [HERE][reference_impl].
## Usage ##
Simply add this crate to your `Cargo.toml` file:
```toml
[dependencies]
paseto = "3.0.0+1.0.3"
```
## Examples ##
The `examples/` directory covers the following use cases:
1. Using the protocol directly to encode potentially non-json data.
2. Using the public builder interface to build a JWT esque equivelant json payload with shared key encryption.
3. Using the public builder interface to build a JWT esque equivelant json payload with public key signing.
[reference_impl]: https://github.com/paragonie/paseto
[reference_license]: https://github.com/paragonie/paseto/blob/master/LICENSE
[blog_post]: https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid
================================================
FILE: examples/direct-protocol.rs
================================================
fn main() {
let key = "YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes();
#[cfg(feature = "v2")]
let mut key_mut = Vec::from(key);
let message = "This is a signed non-JSON message.";
let footer = "key-id:gandalf0";
#[cfg(feature = "v1")]
{
// Version 1
let v1_token = paseto::v1::local::local_paseto(&message, None, key)
.expect("Failed to encrypt V1 Token sans footer.");
println!("{:?}", v1_token);
let decrypted_v1_token = paseto::v1::local::decrypt_paseto(&v1_token, None, key)
.expect("Failed to decrypt V1 Token sans footer.");
println!("{:?}", decrypted_v1_token);
let v1_footer_token = paseto::v1::local::local_paseto(&message, Some(&footer), key)
.expect("Failed to encrypt V1 Token.");
println!("{:?}", v1_footer_token);
let decrypted_v1_footer_token =
paseto::v1::local::decrypt_paseto(&v1_footer_token, Some(&footer), key)
.expect("Failed to decrypt V1 Token.");
println!("{:?}", decrypted_v1_footer_token);
}
#[cfg(feature = "v2")]
{
// Version 2
let v2_token = paseto::v2::local::local_paseto(&message, None, &mut key_mut)
.expect("Failed to encrypt V2 Token sans footer.");
println!("{:?}", v2_token);
let decrypted_v2_token = paseto::v2::local::decrypt_paseto(&v2_token, None, &mut key_mut)
.expect("Failed to decrypt V2 Token sans footer.");
println!("{:?}", decrypted_v2_token);
let v2_footer_token =
paseto::v2::local::local_paseto(&message, Some(&footer), &mut key_mut)
.expect("Failed to encrypt V2 Token.");
println!("{:?}", v2_footer_token);
let decrypted_v2_footer =
paseto::v2::local::decrypt_paseto(&v2_footer_token, Some(&footer), &mut key_mut)
.expect("Failed to decrypt V2 Token.");
println!("{:?}", decrypted_v2_footer);
}
}
================================================
FILE: examples/local-using-builders.rs
================================================
#[cfg(feature = "easy_tokens_chrono")]
use chrono::prelude::*;
#[cfg(any(feature = "easy_tokens_chrono", feature = "easy_tokens_time"))]
use serde_json::json;
#[cfg(feature = "easy_tokens_time")]
use time::{Date, OffsetDateTime, Time};
fn main() {
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
chrono_example();
#[cfg(all(feature = "easy_tokens_time", not(feature = "easy_tokens_chrono")))]
time_example();
#[cfg(all(feature = "easy_tokens_time", feature = "easy_tokens_chrono"))]
chrono_and_time_example();
}
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
fn chrono_example() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() + 1, 7, 8)
.and_hms(9, 10, 11);
let token = paseto::tokens::PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("instructure")
.set_audience("wizards")
.set_jti("gandalf0")
.set_not_before(&Utc::now())
.set_subject("gandalf")
.set_claim("go-to", json!("mordor"))
.set_footer("key-id:gandalf0")
.build()
.expect("Failed to construct paseto token w/ builder!");
println!("{:?}", token);
let verified_token = paseto::tokens::validate_local_token(
&token,
Some("key-id:gandalf0"),
&"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
&paseto::tokens::TimeBackend::Chrono,
)
.expect("Failed to validate token!");
println!("{:?}", verified_token);
}
#[cfg(all(feature = "easy_tokens_time", not(feature = "easy_tokens_chrono")))]
fn time_example() {
let current_date_time = OffsetDateTime::now_utc();
let dt = Date::from_calendar_date(
current_date_time.year() + 1,
current_date_time.month(),
current_date_time.day(),
)
.unwrap()
.with_time(Time::from_hms(09, 10, 11).expect("Failed to create time 09:10:11"))
.assume_utc();
let token = paseto::tokens::PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("instructure")
.set_audience("wizards")
.set_jti("gandalf0")
.set_not_before(&OffsetDateTime::now_utc())
.set_subject("gandalf")
.set_claim("go-to", json!("mordor"))
.set_footer("key-id:gandalf0")
.build()
.expect("Failed to construct paseto token w/ builder!");
println!("{:?}", token);
let verified_token = paseto::tokens::validate_local_token(
&token,
Some("key-id:gandalf0"),
&"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
&paseto::tokens::TimeBackend::Time,
)
.expect("Failed to validate token!");
println!("{:?}", verified_token);
}
#[cfg(all(feature = "easy_tokens_time", feature = "easy_tokens_chrono"))]
fn chrono_and_time_example() {
{
println!("Using Time Crate:");
let current_date_time = OffsetDateTime::now_utc();
let dt = Date::from_calendar_date(
current_date_time.year() + 1,
current_date_time.month(),
current_date_time.day(),
)
.unwrap()
.with_time(Time::from_hms(09, 10, 11).expect("Failed to parse time 09:10:11"))
.assume_utc();
let token = paseto::tokens::PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at_time(None)
.set_expiration_time(&dt)
.set_issuer("instructure")
.set_audience("wizards")
.set_jti("gandalf0")
.set_not_before_time(&OffsetDateTime::now_utc())
.set_subject("gandalf")
.set_claim("go-to", json!("mordor"))
.set_footer("key-id:gandalf0")
.build()
.expect("Failed to construct paseto token w/ builder!");
println!("{:?}", token);
let verified_token = paseto::tokens::validate_local_token(
&token,
Some("key-id:gandalf0"),
&"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
&paseto::tokens::TimeBackend::Time,
)
.expect("Failed to validate token!");
println!("{:?}", verified_token);
}
{
println!("Using Chrono Crate");
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() + 1, 7, 8)
.and_hms(9, 10, 11);
let token = paseto::tokens::PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at_chrono(None)
.set_expiration_chrono(&dt)
.set_issuer("instructure")
.set_audience("wizards")
.set_jti("gandalf0")
.set_not_before_chrono(&Utc::now())
.set_subject("gandalf")
.set_claim("go-to", json!("mordor"))
.set_footer("key-id:gandalf0")
.build()
.expect("Failed to construct paseto token w/ builder!");
println!("{:?}", token);
let verified_token = paseto::tokens::validate_local_token(
&token,
Some("key-id:gandalf0"),
&"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
&paseto::tokens::TimeBackend::Chrono,
)
.expect("Failed to validate token!");
println!("{:?}", verified_token);
}
}
================================================
FILE: examples/public-using-builder.rs
================================================
#[cfg(all(feature = "v2", feature = "easy_tokens_chrono"))]
use chrono::prelude::*;
#[cfg(all(
feature = "v2",
any(feature = "easy_tokens_chrono", feature = "easy_tokens_time")
))]
use serde_json::json;
#[cfg(all(feature = "v2", feature = "easy_tokens_time"))]
use time::{Date, OffsetDateTime, Time};
#[cfg(all(
feature = "v2",
any(feature = "easy_tokens_chrono", feature = "easy_tokens_time")
))]
use {ring::rand::SystemRandom, ring::signature::Ed25519KeyPair};
fn main() {
#[cfg(all(
feature = "v2",
feature = "easy_tokens_chrono",
not(feature = "easy_tokens_time")
))]
chrono_example();
#[cfg(all(
feature = "v2",
feature = "easy_tokens_time",
not(feature = "easy_tokens_chrono")
))]
time_example();
#[cfg(all(
feature = "v2",
feature = "easy_tokens_time",
feature = "easy_tokens_chrono"
))]
chrono_and_time_example();
}
#[cfg(all(
feature = "v2",
feature = "easy_tokens_chrono",
not(feature = "easy_tokens_time")
))]
fn chrono_example() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() + 1, 7, 8)
.and_hms(9, 10, 11);
let sys_rand = SystemRandom::new();
let key_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!");
let as_key = Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair");
let token = paseto::tokens::PasetoBuilder::new()
.set_ed25519_key(&as_key)
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("instructure")
.set_audience("wizards")
.set_jti("gandalf0")
.set_not_before(&Utc::now())
.set_subject("gandalf")
.set_claim("go-to", json!("mordor"))
.set_footer("key-id:gandalf0")
.build()
.expect("Failed to construct paseto token w/ builder!");
println!("{:?}", token);
let verified_token = paseto::tokens::validate_public_token(
&token,
Some("key-id:gandalf0"),
&paseto::tokens::PasetoPublicKey::ED25519KeyPair(&as_key),
&paseto::tokens::TimeBackend::Chrono,
)
.expect("Failed to validate token!");
println!("{:?}", verified_token);
}
#[cfg(all(
feature = "v2",
feature = "easy_tokens_time",
not(feature = "easy_tokens_chrono")
))]
fn time_example() {
let current_date_time = OffsetDateTime::now_utc();
let dt = Date::from_calendar_date(
current_date_time.year() + 1,
current_date_time.month(),
current_date_time.day(),
)
.unwrap()
.with_time(Time::from_hms(09, 10, 11).expect("Failed to create time 09:10:11"))
.assume_utc();
let sys_rand = SystemRandom::new();
let key_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!");
let as_key = Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair");
let token = paseto::tokens::PasetoBuilder::new()
.set_ed25519_key(&as_key)
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("instructure")
.set_audience("wizards")
.set_jti("gandalf0")
.set_not_before(&OffsetDateTime::now_utc())
.set_subject("gandalf")
.set_claim("go-to", json!("mordor"))
.set_footer("key-id:gandalf0")
.build()
.expect("Failed to construct paseto token w/ builder!");
println!("{:?}", token);
let verified_token = paseto::tokens::validate_public_token(
&token,
Some("key-id:gandalf0"),
&paseto::tokens::PasetoPublicKey::ED25519KeyPair(&as_key),
&paseto::tokens::TimeBackend::Time,
)
.expect("Failed to validate token!");
println!("{:?}", verified_token);
}
#[cfg(all(
feature = "v2",
feature = "easy_tokens_time",
feature = "easy_tokens_chrono"
))]
fn chrono_and_time_example() {
{
println!("Using Time Crate!");
let current_date_time = OffsetDateTime::now_utc();
let dt = Date::from_calendar_date(
current_date_time.year() + 1,
current_date_time.month(),
current_date_time.day(),
)
.unwrap()
.with_time(Time::from_hms(09, 10, 11).expect("Failed to parse time 09:10:11"))
.assume_utc();
let sys_rand = SystemRandom::new();
let key_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!");
let as_key =
Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair");
let token = paseto::tokens::PasetoBuilder::new()
.set_ed25519_key(&as_key)
.set_issued_at_time(None)
.set_expiration_time(&dt)
.set_issuer("instructure")
.set_audience("wizards")
.set_jti("gandalf0")
.set_not_before_time(&OffsetDateTime::now_utc())
.set_subject("gandalf")
.set_claim("go-to", json!("mordor"))
.set_footer("key-id:gandalf0")
.build()
.expect("Failed to construct paseto token w/ builder!");
println!("{:?}", token);
let verified_token = paseto::tokens::validate_public_token(
&token,
Some("key-id:gandalf0"),
&paseto::tokens::PasetoPublicKey::ED25519KeyPair(&as_key),
&paseto::tokens::TimeBackend::Time,
)
.expect("Failed to validate token!");
println!("{:?}", verified_token);
}
{
println!("Using Chrono Crate!");
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() + 1, 7, 8)
.and_hms(9, 10, 11);
let sys_rand = SystemRandom::new();
let key_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!");
let as_key =
Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair");
let token = paseto::tokens::PasetoBuilder::new()
.set_ed25519_key(&as_key)
.set_issued_at_chrono(None)
.set_expiration_chrono(&dt)
.set_issuer("instructure")
.set_audience("wizards")
.set_jti("gandalf0")
.set_not_before_chrono(&Utc::now())
.set_subject("gandalf")
.set_claim("go-to", json!("mordor"))
.set_footer("key-id:gandalf0")
.build()
.expect("Failed to construct paseto token w/ builder!");
println!("{:?}", token);
let verified_token = paseto::tokens::validate_public_token(
&token,
Some("key-id:gandalf0"),
&paseto::tokens::PasetoPublicKey::ED25519KeyPair(&as_key),
&paseto::tokens::TimeBackend::Chrono,
)
.expect("Failed to validate token!");
println!("{:?}", verified_token);
}
}
================================================
FILE: rustfmt.toml
================================================
edition = "2018"
hard_tabs = true
max_width = 100
newline_style = "Unix"
================================================
FILE: src/errors.rs
================================================
//! A package that contains all of the error types this crate is capable of
//! producing.
use thiserror::Error;
/// The 'top-level' error that each function will return, this is just here to
/// give us one consistent return type.
#[derive(Debug, Error)]
pub enum PasetoError {
#[cfg(any(feature = "easy_tokens_chrono", feature = "easy_tokens_time"))]
#[error(transparent)]
/// An error re-exported from the `serde_json` crate.
JsonError(#[from] serde_json::Error),
#[error(transparent)]
/// There was an error that was just a generic paseto error, not speciifc
/// to any library or encryption.
GenericError(#[from] GenericError),
#[error(transparent)]
/// There was an error interacting with libsodium.
LibsodiumError(#[from] SodiumErrors),
#[cfg(feature = "v1")]
#[error(transparent)]
/// There was an error interacting with OpenSSL.
OpensslError(#[from] openssl::error::ErrorStack),
#[error(transparent)]
/// There was an error related to RSA key parsing.
RsaError(#[from] RsaKeyErrors),
}
/// There was an error interacting with libsodium.
#[derive(Debug, Error)]
pub enum SodiumErrors {
/// A libsodium algorithim was expecting a key of a certain size, but did
/// not receive it.
#[error("invalid key size, needed: {} got: {}", size_needed, size_provided)]
InvalidKeySize {
size_provided: usize,
size_needed: usize,
},
/// A libsodium algorithim was expecting a nonce of a certain size, but did
/// not receive it.
#[error("invalid nonce size, needed: {} got: {}", size_needed, size_provided)]
InvalidNonceSize {
size_provided: usize,
size_needed: usize,
},
/// the libsodium algorithim let us know the key was invalid with no extra info.
#[error("Invalid key for libsodium!")]
InvalidKey {},
/// A C function from libsodium we never expect to fail, failed.
#[error("Function call to C Sodium Failed.")]
FunctionError {},
/// libsodium expected to write a certain number of bytes out, but
/// couldn't write because it was too small or too large.
#[error("Invalid Output Size specified")]
InvalidOutputSize {},
}
/// There was an error roughly related to the RSA Key input.
#[derive(Debug, Error)]
pub enum RsaKeyErrors {
/// The user provided key could not be parsed as DER.
#[error("Invalid RSA Key Provided")]
InvalidKey {},
/// We could not sign the data using the key provided.
#[error("Failed to generate signed RSA content")]
SignError {},
}
/// Any errors that are generic to the whole crate.
#[derive(Debug, Error)]
pub enum GenericError {
/// We failed to create a key with hmac's or HKDF.
#[error("Failed to perform HKDF")]
BadHkdf {},
/// An error re-exported from the base64 crate.
#[error(transparent)]
Base64Error(#[from] base64::DecodeError),
/// While validating a token we noticed it was expired.
#[error("This token is expired (EXP claim).")]
ExpiredToken {},
/// While validating a token we expected a specific footer, but noticed
/// it was not correct.
#[error("This token has an invalid footer.")]
InvalidFooter {},
/// While validating a token we noticed the issued at timestamp was in
/// the future meaning it has not yet been issued.
#[error("This token has not yet been issued, the issued at claim (IAT) is in the future.")]
InvalidIssuedAtToken {},
/// While validating a token, the not before claim was in the future,
/// which means the token cannot yet be used.
#[error("This token is not valid yet (NBF claim).")]
InvalidNotBeforeToken {},
/// While attempting to validate a token we could not parse/decrypt/validate the signature
/// of the token.
#[error("This token is invalid.")]
InvalidToken {},
/// While using the 'easy' token builder pattern you failed
/// to provide a key that could be used given your compile time
/// options.
#[error("No key of the correct type was provided")]
NoKeyProvided {},
/// We failed generating the amount of random bytes that we needed.
#[error("Failed to generate enough random bytes.")]
RandomError {},
/// Re-Exporting a string from utf8 error.
#[error(transparent)]
Utf8Error(#[from] std::string::FromUtf8Error),
/// We expected a datetime in a specific claim but failed to parse the data
/// as a date.
#[error("The claim: {}, has an invalid date", claim_name)]
UnparseableTokenDate { claim_name: &'static str },
}
================================================
FILE: src/lib.rs
================================================
//! Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of
//! the many design deficits that plague the JOSE standards.
//!
//! # Getting Started
//!
//! The Paseto crate provides Builders which can help in the ease of building
//! tokens in an ergonomic way, these are enabled by default. However, they
//! do bring in extra dependencies on [`serde_json`], along with one of two
//! time crates [`chrono`] (the default), or if the user wants `time`.
//!
//! These can be turned on by enabling one of: `easy_tokens_chrono`, or
//! `easy_tokens_time`. While it is possible for both to be on at the same
//! time you really should only enable one. The examples below will use
//! `easy_tokens_chrono` since that is by default enabled.
//!
//! ## Creating a Token With a Builder
//!
//! Creating a token with easy tokens is done through the 'builder' pattern
//! which allows you to seemlessly build out a token, an example might be
//! as follows:
//!
//! ```
//! # #[cfg(all(feature = "easy_tokens_chrono", feature = "v2", feature = "v1", not(feature = "easy_tokens_time")))] {
//! use chrono::prelude::*;
//! use serde_json::json;
//!
//! let current_date_time = Utc::now();
//! let a_year_from_today = Utc
//! .ymd(current_date_time.year() + 1, current_date_time.month(), current_date_time.day())
//! .and_hms(0, 0, 0);
//!
//! let token = paseto::tokens::PasetoBuilder::new()
//! .set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
//! .set_issued_at(None) // let's someone know when we created this token.
//! .set_expiration(&a_year_from_today) // set this to expire in a year
//! .set_issuer("instructure") // instructure issued this token
//! .set_audience("witches") // this token is meant for witches
//! .set_jti("gandalf0") // "JTI" is a jwt token id.
//! .set_not_before(&Utc::now()) // don't let someone use this token before now.
//! .set_subject("gandalf") // this token belongs to gandolf
//! .set_claim("go-to", json!("mordor")) // an extra claim useful to the application
//! .set_footer("key-id:gandalf0") // include the key-id in the footer for easy distinction.
//! .build()
//! .expect("Failed to encrypt token!");
//! # }
//! ```
//!
//! ## Creating a Token Without a Builder
//!
//! If you do not wish to use the builder pattern you can manually encrypt,
//! or sign a token. In this mode you are choosing directly what to stuff
//! in the body (it does not have to be JSON), as well as which particular
//! version of Paseto you're using directly.
//!
//! To know which version to choose we recommend reading:
//! <https://github.com/paragonie/paseto/blob/master/docs/Features.md>
//!
//! In this example we'll be using a `v2.local` token, but the process is
//! similar for each:
//!
//! ```
//! # #[cfg(all(feature = "easy_tokens_chrono", feature = "v2", feature = "v1", not(feature = "easy_tokens_time")))] {
//! let mut key = Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes());
//! let v2_token = paseto::v2::local::local_paseto(
//! r#"{"my": "secret", "data": "here"}"#,
//! Some("my-footer"),
//! &mut key
//! ).expect("Failed to encrypt V2 Token.");
//! # }
//! ```
//!
//! Similar versions can be used such as [`v1::local::local_paseto`],
//! [`v2::public::public_paseto`], or [`v1::public::public_paseto`].
//!
//! ## Verifying a Token (With Extra Checks)
//!
//! If you're using the:
//!
//! - Builder Pattern
//! - JSON tokens, but have one of the build time features `easy_tokens_chrono` (on by default), or `easy_tokens_time`
//!
//! There are a series of methods that will automatically give you a JSON value,
//! and automatically validate the fields: `iat` (issued at), `exp` (expires), `nbf` (not before) for you.
//! This is in general a much *safer, and cleaner* way of validating the data within the
//! token.
//!
//! To do this you can use [`tokens::validate_local_token`], and [`tokens::validate_public_token`]
//! respectively. For example to validate the local token we created with our builder:
//!
//! ```
//! # #[cfg(all(feature = "easy_tokens_chrono", feature = "v2", feature = "v1", not(feature = "easy_tokens_time")))] {
//! # use chrono::prelude::*;
//! # use serde_json::json;
//! #
//! # let current_date_time = Utc::now();
//! # let a_year_from_today = Utc
//! # .ymd(current_date_time.year() + 1, current_date_time.month(), current_date_time.day())
//! # .and_hms(0, 0, 0);
//! #
//! # let token = paseto::tokens::PasetoBuilder::new()
//! # .set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
//! # .set_issued_at(None) // let's someone know when we created this token.
//! # .set_expiration(&a_year_from_today) // set this to expire in a year
//! # .set_issuer("instructure") // instructure issued this token
//! # .set_audience("witches") // this token is meant for witches
//! # .set_jti("gandalf0") // "JTI" is a concept of JWT's and is meant to uniquely identify the token.
//! # .set_not_before(&Utc::now()) // don't let someone use this token before now.
//! # .set_subject("gandalf") // this token belongs to gandolf
//! # .set_claim("go-to", json!("mordor")) // an extra claim useful to the application
//! # .set_footer("key-id:gandalf0") // include the key-id in the footer for easy distinction.
//! # .build()
//! # .expect("Failed to encrypt token!");
//! #
//! let verified_token = paseto::tokens::validate_local_token(
//! &token,
//! Some("key-id:gandalf0"),
//! &"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
//! &paseto::tokens::TimeBackend::Chrono,
//! )
//! .expect("Failed to validate token!");
//! # }
//! ```
//!
//! ## Decrypting/Validating Signature for a Token
//!
//! If you do not have `easy_tokens_chrono`/`easy_tokens_time` enabled, or
//! are validating something that isn't JSON. You can still use the 'direct'
//! methods but ***you own all validation of the data inside the token.*** This
//! means if you're using these methods ***it is up to you to check things like
//! expires timestamp***.
//!
//! Using the token we built above without a builder we can validate it like so:
//!
//! ```rust
//! # #[cfg(all(feature = "easy_tokens_chrono", feature = "v2", feature = "v1", not(feature = "easy_tokens_time")))] {
//! # let mut key = Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes());
//! # let v2_token = paseto::v2::local::local_paseto(r#"{"my": "secret", "data": "here"}"#, Some("my-footer"), &mut key).expect("Failed to encrypt V2 Token.");
//! #
//! let decrypted_v2_token = paseto::v2::local::decrypt_paseto(
//! &v2_token,
//! Some("my-footer"),
//! &mut key
//! ).expect("Failed to decrypt V2 Token.");
//! # }
//! ```
//!
//! Again at this point the token is decrypted but is up to you, to validate it's contents.
#![recursion_limit = "128"]
#![allow(
clippy::match_wildcard_for_single_variants,
clippy::module_name_repetitions
)]
pub mod errors;
pub mod pae;
#[cfg(any(feature = "easy_tokens_chrono", feature = "easy_tokens_time"))]
pub mod tokens;
#[cfg(any(feature = "easy_tokens_chrono", feature = "easy_tokens_time"))]
pub use self::tokens::*;
#[cfg(feature = "v1")]
pub mod v1;
#[cfg(feature = "v2")]
pub mod v2;
================================================
FILE: src/pae.rs
================================================
//! Implements "Pre-Authentication Encoding". An encoding scheme unique to
//! paseto. Generally not useful outside of this crate.
//!
//! Pre-Authentication Encoding is an encoding mechanism meant to help
//! prevent canonicalization attacks for local tokens in the additional
//! data field. For more information see:
//! <https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b058649d01a82a8c412087bc87/docs/01-Protocol-Versions/Common.md#authentication-padding>
/// Performs little endian encoding of an unsigned 64 bit integer.
#[allow(clippy::cast_possible_truncation)]
fn le64(mut to_encode: u64) -> Vec<u8> {
let mut the_vec = Vec::with_capacity(8);
for _idx in 0..8 {
the_vec.push((to_encode & 255) as u8);
to_encode >>= 8;
}
the_vec
}
/// Performs the actual pre authentication encoding for a list of binary
/// strings.
#[must_use]
pub fn pae(pieces: &[&[u8]]) -> Vec<u8> {
let the_vec = le64(pieces.len() as u64);
pieces.iter().fold(the_vec, |mut acc, piece| {
acc.extend(le64(piece.len() as u64));
acc.extend(piece.iter());
acc
})
}
#[cfg(test)]
mod unit_tests {
use super::*;
use hex;
#[test]
fn test_le64() {
assert_eq!(vec![0, 0, 0, 0, 0, 0, 0, 0], le64(0));
assert_eq!(vec![10, 0, 0, 0, 0, 0, 0, 0], le64(10));
}
#[test]
fn test_pae() {
// Constants taken from paseto source.
assert_eq!("0000000000000000", hex::encode(&pae(&[])));
assert_eq!(
"01000000000000000000000000000000",
hex::encode(&pae(&[&[]]))
);
assert_eq!(
"020000000000000000000000000000000000000000000000",
hex::encode(&pae(&[&[], &[]]))
);
assert_eq!(
"0100000000000000070000000000000050617261676f6e",
hex::encode(&pae(&["Paragon".as_bytes()]))
);
assert_eq!(
"0200000000000000070000000000000050617261676f6e0a00000000000000496e6974696174697665",
hex::encode(&pae(&["Paragon".as_bytes(), "Initiative".as_bytes(),]))
);
}
}
================================================
FILE: src/tokens/builder.rs
================================================
//! The token "builder" which makes it easy to create JSON encoded Paseto
//! tokens.
#[cfg(feature = "v1")]
use crate::errors::RsaKeyErrors;
use crate::errors::{GenericError, PasetoError};
#[cfg(all(not(feature = "v2"), feature = "v1"))]
use crate::v1::local_paseto as V1Local;
#[cfg(feature = "v1")]
use crate::v1::public_paseto as V1Public;
#[cfg(feature = "v2")]
use crate::v2::{local_paseto as V2Local, public_paseto as V2Public};
#[cfg(feature = "easy_tokens_chrono")]
use chrono::prelude::*;
#[cfg(feature = "v2")]
use ring::signature::Ed25519KeyPair;
#[cfg(feature = "v1")]
use ring::signature::RsaKeyPair;
use serde_json::{json, to_string, Value};
#[cfg(feature = "easy_tokens_time")]
use time::OffsetDateTime;
use std::collections::HashMap;
/// A structure that implements the builder pattern for building a Paseto token.
pub struct PasetoBuilder<'a> {
/// Set the footer to use for this token.
footer: Option<&'a str>,
/// The encryption key to use. If present WILL use LOCAL tokens (or shared key encryption).
encryption_key: Option<&'a [u8]>,
/// The RSA Key pairs in DER format, for V1 Public Tokens.
#[cfg(feature = "v1")]
rsa_key: Option<&'a [u8]>,
/// The ED25519 Key Pair, for V2 Public Tokens.
#[cfg(feature = "v2")]
ed_key: Option<&'a Ed25519KeyPair>,
/// Any extra claims you want to store in your json.
extra_claims: HashMap<&'a str, Value>,
}
impl<'a> PasetoBuilder<'a> {
/// Create a new token builder with no attributes set.
#[must_use]
pub fn new() -> PasetoBuilder<'a> {
PasetoBuilder {
footer: None,
encryption_key: None,
#[cfg(feature = "v1")]
rsa_key: None,
#[cfg(feature = "v2")]
ed_key: None,
extra_claims: HashMap::new(),
}
}
/// Attempt to 'build' or create the final token.
///
/// # Errors
///
/// - If you failed to set an encryption key.
/// - If the underlying encryption, or signing failed to be completed.
pub fn build(&self) -> Result<String, PasetoError> {
let strd_msg = to_string(&self.extra_claims)?;
#[cfg(feature = "v2")]
{
if let Some(enc_key) = self.encryption_key {
return V2Local(&strd_msg, self.footer.as_deref(), enc_key);
}
}
#[cfg(all(not(feature = "v2"), feature = "v1"))]
{
if let Some(enc_key) = self.encryption_key {
return V1Local(&strd_msg, self.footer.as_deref(), enc_key);
}
}
#[cfg(feature = "v2")]
{
if let Some(ed_key_pair) = self.ed_key {
return V2Public(&strd_msg, self.footer.as_deref(), ed_key_pair);
}
}
#[cfg(feature = "v1")]
{
if let Some(the_rsa_key) = self.rsa_key {
return match RsaKeyPair::from_der(the_rsa_key) {
Ok(key_pair) => V1Public(&strd_msg, self.footer.as_deref(), &key_pair),
Err(_) => Err(RsaKeyErrors::InvalidKey {}.into()),
};
}
}
Err(GenericError::NoKeyProvided {}.into())
}
}
impl<'a> Default for PasetoBuilder<'a> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "v1")]
impl<'a> PasetoBuilder<'a> {
/// Sets the RSA Key on a Paseto builder.
///
/// ***NOTE: This will not be used if you set a symmetric encryption key, or if you specify an Ed25519 key pair.***
pub fn set_rsa_key(&'a mut self, private_key_der: &'a [u8]) -> &'a mut Self {
self.rsa_key = Some(private_key_der);
self
}
}
#[cfg(feature = "v2")]
impl<'a> PasetoBuilder<'a> {
/// Sets the ED25519 Key pair.
///
/// ***NOTE: This will not be used if you set a symmetric encryption key.***
pub fn set_ed25519_key(&'a mut self, key_pair: &'a Ed25519KeyPair) -> &'a mut Self {
self.ed_key = Some(key_pair);
self
}
}
impl<'a> PasetoBuilder<'a> {
/// Sets the encryption key to use for the paseto token.
///
/// Keys should be exactly 32 bytes long, this is a requirement of the
/// underlying encryption algorithims.
///
/// ***NOTE: If you set this we _*will*_ use a local token.***
pub fn set_encryption_key(&'a mut self, encryption_key: &'a [u8]) -> &'a mut Self {
self.encryption_key = Some(encryption_key);
self
}
//// Sets the footer to use for this token.
pub fn set_footer(&'a mut self, footer: &'a str) -> &'a mut Self {
self.footer = Some(footer);
self
}
/// Provide multiple arbitrary claims at once, if a key is already present it will be updated.
///
/// Mirrors [`std::iter::Extend`] for normal maps.
pub fn extend_claims(&'a mut self, claims: HashMap<&'a str, Value>) -> &'a mut Self {
self.extra_claims.extend(claims);
self
}
/// Sets an arbitrary claim (a key inside the json token).
pub fn set_claim(&'a mut self, key: &'a str, value: Value) -> &'a mut Self {
self.extra_claims.insert(key, value);
self
}
/// Sets the audience for this token.
pub fn set_audience(&'a mut self, audience: &str) -> &'a mut Self {
self.set_claim("aud", json!(audience))
}
/// Sets the expiration date for this token.
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
pub fn set_expiration(&'a mut self, expiration: &DateTime<Utc>) -> &'a mut Self {
self.set_claim("exp", json!(expiration))
}
/// Sets the expiration date for this token.
#[cfg(all(feature = "easy_tokens_time", not(feature = "easy_tokens_chrono")))]
pub fn set_expiration(&'a mut self, expiration: &OffsetDateTime) -> &'a mut Self {
self.set_claim("exp", json!(expiration))
}
/// Sets the expiration date for this token.
#[cfg(all(feature = "easy_tokens_chrono", feature = "easy_tokens_time"))]
pub fn set_expiration_chrono(&'a mut self, expiration: &DateTime<Utc>) -> &'a mut Self {
self.set_claim("exp", json!(expiration))
}
/// Sets the expiration date for this token.
#[cfg(all(feature = "easy_tokens_time", feature = "easy_tokens_time"))]
pub fn set_expiration_time(&'a mut self, expiration: &OffsetDateTime) -> &'a mut Self {
self.set_claim("exp", json!(expiration))
}
/// Sets the time this token was issued at.
///
/// `issued_at` defaults to: `Utc::now()`
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
pub fn set_issued_at(&'a mut self, issued_at: Option<DateTime<Utc>>) -> &'a mut Self {
self.set_claim("iat", json!(issued_at.unwrap_or_else(Utc::now)))
}
/// Sets the time this token was issued at.
///
/// `issued_at` defaults to: `OffsetDateTime::now_utc()`
#[cfg(all(feature = "easy_tokens_time", not(feature = "easy_tokens_chrono")))]
pub fn set_issued_at(&'a mut self, issued_at: Option<OffsetDateTime>) -> &'a mut Self {
self.set_claim(
"iat",
json!(issued_at.unwrap_or_else(OffsetDateTime::now_utc)),
)
}
/// Sets the time this token was issued at.
///
/// `issued_at` defaults to: `Utc::now()`
#[cfg(all(feature = "easy_tokens_chrono", feature = "easy_tokens_time"))]
pub fn set_issued_at_chrono(&'a mut self, issued_at: Option<DateTime<Utc>>) -> &'a mut Self {
self.set_claim("iat", json!(issued_at.unwrap_or_else(Utc::now)))
}
/// Sets the time this token was issued at.
///
/// `issued_at` defaults to: `OffsetDateTime::now_utc()`
#[cfg(all(feature = "easy_tokens_time", feature = "easy_tokens_time"))]
pub fn set_issued_at_time(&'a mut self, issued_at: Option<OffsetDateTime>) -> &'a mut Self {
self.set_claim(
"iat",
json!(issued_at.unwrap_or_else(OffsetDateTime::now_utc)),
)
}
/// Sets the issuer for this token.
pub fn set_issuer(&'a mut self, issuer: &str) -> &'a mut Self {
self.set_claim("iss", json!(issuer))
}
/// Sets the JTI ID for this token.
pub fn set_jti(&'a mut self, id: &str) -> &'a mut Self {
self.set_claim("jti", json!(id))
}
/// Sets the not before time.
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
pub fn set_not_before(&'a mut self, not_before: &DateTime<Utc>) -> &'a mut Self {
self.set_claim("nbf", json!(not_before))
}
/// Sets the not before time.
#[cfg(all(feature = "easy_tokens_time", not(feature = "easy_tokens_chrono")))]
pub fn set_not_before(&'a mut self, not_before: &OffsetDateTime) -> &'a mut Self {
self.set_claim("nbf", json!(not_before))
}
/// Sets the not before time.
#[cfg(all(feature = "easy_tokens_chrono", feature = "easy_tokens_time"))]
pub fn set_not_before_chrono(&'a mut self, not_before: &DateTime<Utc>) -> &'a mut Self {
self.set_claim("nbf", json!(not_before))
}
/// Sets the not before time.
#[cfg(all(feature = "easy_tokens_time", feature = "easy_tokens_time"))]
pub fn set_not_before_time(&'a mut self, not_before: &OffsetDateTime) -> &'a mut Self {
self.set_claim("nbf", json!(not_before))
}
/// Sets the subject for this token.
pub fn set_subject(&'a mut self, subject: &str) -> &'a mut Self {
self.set_claim("sub", json!(subject))
}
}
#[cfg(test)]
mod unit_test {
#[cfg(feature = "v2")]
use {
super::*, crate::v2::local::decrypt_paseto as V2Decrypt, serde_json::from_str as ParseJson,
};
#[test]
#[cfg(all(
feature = "v2",
feature = "easy_tokens_chrono",
not(feature = "easy_tokens_time")
))]
fn can_construct_a_token_chrono_with_extend_claims() {
//set or get a map of any number of claims
let mut arbitrary_claims = HashMap::new();
arbitrary_claims.insert("claim1", json!("data1"));
arbitrary_claims.insert("claim2", json!("data2"));
arbitrary_claims.insert("claim3", json!("data3"));
arbitrary_claims.insert("claim4", json!("data4"));
arbitrary_claims.insert("claim5", json!("data5"));
let token = PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&Utc::now())
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&Utc::now())
.set_subject("test")
.extend_claims(arbitrary_claims)
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
let decrypted_token = V2Decrypt(
&token,
Some("footer"),
&mut Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()),
)
.expect("Failed to decrypt token constructed with builder!");
let parsed: Value =
ParseJson(&decrypted_token).expect("Failed to parse finalized token as json!");
assert!(parsed.get("claim1").is_some());
assert!(parsed.get("claim2").is_some());
assert!(parsed.get("claim3").is_some());
assert!(parsed.get("claim4").is_some());
assert!(parsed.get("claim5").is_some());
assert!(parsed.get("claim6").is_none());
}
#[test]
#[cfg(all(
feature = "v2",
feature = "easy_tokens_chrono",
not(feature = "easy_tokens_time")
))]
fn can_construct_a_token_chrono() {
let token = PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&Utc::now())
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&Utc::now())
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
let decrypted_token = V2Decrypt(
&token,
Some("footer"),
&mut Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()),
)
.expect("Failed to decrypt token constructed with builder!");
let parsed: Value =
ParseJson(&decrypted_token).expect("Failed to parse finalized token as json!");
assert!(parsed.get("iat").is_some());
assert!(parsed.get("iss").is_some());
assert!(parsed.get("aud").is_some());
assert!(parsed.get("jti").is_some());
assert!(parsed.get("sub").is_some());
assert!(parsed.get("claim").is_some());
assert!(parsed.get("nbf").is_some());
}
#[test]
#[cfg(all(
feature = "v2",
feature = "easy_tokens_time",
not(feature = "easy_tokens_chrono")
))]
fn can_construct_a_token_time() {
let token = PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&OffsetDateTime::now_utc())
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&OffsetDateTime::now_utc())
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
let decrypted_token = V2Decrypt(
&token,
Some("footer"),
&mut Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()),
)
.expect("Failed to decrypt token constructed with builder!");
let parsed: Value =
ParseJson(&decrypted_token).expect("Failed to parse finalized token as json!");
assert!(parsed.get("iat").is_some());
assert!(parsed.get("iss").is_some());
assert!(parsed.get("aud").is_some());
assert!(parsed.get("jti").is_some());
assert!(parsed.get("sub").is_some());
assert!(parsed.get("claim").is_some());
assert!(parsed.get("nbf").is_some());
}
}
================================================
FILE: src/tokens/mod.rs
================================================
//! "Easy Tokens", or helper functions to make building/validating tokens easier.
//!
//! The token builder is in general the recommended way to build, and validate
//! paseto tokens. Not only does it do things like validate expirey time on verification
//! It gives you a nice builder pattern to make setting common claims easier.
//!
//! See:
//!
//! - [`self::builder::PasetoBuilder`]: Building a token of any kind.
//! - [`self::validate_local_token`]: validating a: `(v1.v2).local.` paseto token.
//! - [`self::validate_public_token`]: validating a: `(v1.v2).public.` paseto token.
//! - [`self::validate_potential_json_blob`]: if you manually decrypted a token, and just want to validate the JSON body.
use crate::errors::{GenericError, PasetoError};
#[cfg(feature = "v1")]
use crate::v1::{decrypt_paseto as V1Decrypt, verify_paseto as V1Verify};
#[cfg(feature = "v2")]
use crate::v2::{decrypt_paseto as V2Decrypt, verify_paseto as V2Verify};
#[cfg(feature = "easy_tokens_chrono")]
use chrono::prelude::*;
#[cfg(feature = "v2")]
use ring::signature::{Ed25519KeyPair, KeyPair};
use serde_json::{from_str as ParseJson, Value as JsonValue};
#[cfg(feature = "easy_tokens_time")]
use time::OffsetDateTime;
pub mod builder;
pub use self::builder::*;
/// A small wrapper around all the types of public keys that can be used for
/// signing data. For Algorithim Lucidity, and ease of APIs.
pub enum PasetoPublicKey<'a> {
/// A RSA Public Key in DER format as a byte array.
#[cfg(feature = "v1")]
RSAPublicKey(&'a [u8]),
/// An ED25519 public key, but pass in the key pair for api ease.
#[cfg(feature = "v2")]
ED25519KeyPair(&'a Ed25519KeyPair),
/// An ED25519 Public Key as a byte array.
#[cfg(feature = "v2")]
ED25519PublicKey(&'a [u8]),
}
/// Specifies which time crate will be used as backend for validating a token's
/// datetimes, e.g. `issued_at`. The available backends are [`Chrono`] and [`Time`],
/// the can be enabled via the features `easy_tokens_chrono` and `easy_tokens_time`.
/// The default feature and backend is [`Chrono`].
///
/// [`Chrono`]: https://docs.rs/chrono/*/chrono/index.html
/// [`Time`]: https://docs.rs/time/*/time/index.html
pub enum TimeBackend {
#[cfg(feature = "easy_tokens_chrono")]
Chrono,
#[cfg(feature = "easy_tokens_time")]
Time,
}
/// Validates a potential json data blob, returning a [`JsonValue`].
///
/// This specifically validates:
/// * `iat` (issued at)
/// * `exp` (expired)
/// * `nbf` (not before)
///
/// This specifically does not validate:
/// * `audience`
/// * `jti`
/// * `issuedBy`
/// * `subject`
///
/// # Errors
///
/// - if the data of the token is not valid JSON
/// - the json contains invalid `iat`, `exp`, or `nbf` values.
pub fn validate_potential_json_blob(
data: &str,
backend: &TimeBackend,
) -> Result<JsonValue, PasetoError> {
let value: JsonValue = ParseJson(data)?;
match backend {
#[cfg(feature = "easy_tokens_chrono")]
TimeBackend::Chrono => {
let iat_value = value.get("iat");
if iat_value.is_some() {
let parsed_iat = iat_value
.and_then(serde_json::Value::as_str)
.ok_or(GenericError::UnparseableTokenDate { claim_name: "iat" })
.and_then(|iat| {
iat.parse::<DateTime<Utc>>()
.map_err(|_| GenericError::UnparseableTokenDate { claim_name: "iat" })
})?;
if parsed_iat > Utc::now() {
return Err(GenericError::InvalidIssuedAtToken {}.into());
}
}
let exp_value = value.get("exp");
if exp_value.is_some() {
let parsed_exp = exp_value
.and_then(serde_json::Value::as_str)
.ok_or(GenericError::UnparseableTokenDate { claim_name: "exp" })
.and_then(|exp| {
exp.parse::<DateTime<Utc>>()
.map_err(|_| GenericError::UnparseableTokenDate { claim_name: "exp" })
})?;
if parsed_exp < Utc::now() {
return Err(GenericError::ExpiredToken {}.into());
}
}
let nbf_value = value.get("nbf");
if nbf_value.is_some() {
let parsed_nbf = nbf_value
.and_then(serde_json::Value::as_str)
.ok_or(GenericError::UnparseableTokenDate { claim_name: "nbf" })
.and_then(|nbf| {
nbf.parse::<DateTime<Utc>>()
.map_err(|_| GenericError::UnparseableTokenDate { claim_name: "nbf" })
})?;
if parsed_nbf > Utc::now() {
return Err(GenericError::InvalidNotBeforeToken {}.into());
}
}
Ok(value)
}
#[cfg(feature = "easy_tokens_time")]
TimeBackend::Time => {
let iat_value = value.get("iat");
if iat_value.is_some() {
let parsed_iat = iat_value
.and_then(serde_json::Value::as_str)
.ok_or(GenericError::UnparseableTokenDate { claim_name: "iat" })
.and_then(|iat| {
OffsetDateTime::parse(iat, &time::format_description::well_known::Rfc3339)
.map_err(|_| GenericError::UnparseableTokenDate { claim_name: "iat" })
})?;
if parsed_iat > OffsetDateTime::now_utc() {
return Err(GenericError::InvalidIssuedAtToken {}.into());
}
}
let exp_value = value.get("exp");
if exp_value.is_some() {
let parsed_exp = exp_value
.and_then(serde_json::Value::as_str)
.ok_or(GenericError::UnparseableTokenDate { claim_name: "exp" })
.and_then(|exp| {
OffsetDateTime::parse(exp, &time::format_description::well_known::Rfc3339)
.map_err(|_| GenericError::UnparseableTokenDate { claim_name: "exp" })
})?;
if parsed_exp < OffsetDateTime::now_utc() {
return Err(GenericError::ExpiredToken {}.into());
}
}
let nbf_value = value.get("nbf");
if nbf_value.is_some() {
let parsed_nbf = nbf_value
.and_then(serde_json::Value::as_str)
.ok_or(GenericError::UnparseableTokenDate { claim_name: "nbf" })
.and_then(|nbf| {
OffsetDateTime::parse(nbf, &time::format_description::well_known::Rfc3339)
.map_err(|_| GenericError::UnparseableTokenDate { claim_name: "nbf" })
})?;
if parsed_nbf > OffsetDateTime::now_utc() {
return Err(GenericError::InvalidNotBeforeToken {}.into());
}
}
Ok(value)
}
}
}
/// Validate a local token for V1, or V2.
///
/// This specifically validates:
/// * `iat` (issued at)
/// * `exp` (expired)
/// * `nbf` (not before)
///
/// This specifically does not validate:
/// * `audience`
/// * `jti`
/// * `issuedBy`
/// * `subject`
///
/// Because we validate these fields the resulting type must be a json object. If it's not
/// please use the protocol impls directly.
///
/// # Errors
///
/// 1. If we fail to decrypt the local token.
/// 2. If any of the token fields are invalid: `iat`, `exp`, or `nbf`.
pub fn validate_local_token(
token: &str,
footer: Option<&str>,
key: &[u8],
backend: &TimeBackend,
) -> Result<JsonValue, PasetoError> {
#[cfg(feature = "v2")]
{
if token.starts_with("v2.local.") {
let message = V2Decrypt(token, footer, key)?;
return validate_potential_json_blob(&message, backend);
}
}
#[cfg(feature = "v1")]
{
if token.starts_with("v1.local.") {
let message = V1Decrypt(token, footer, key)?;
return validate_potential_json_blob(&message, backend);
}
}
Err(GenericError::InvalidToken {}.into())
}
/// Validate a public token for V1, or V2.
///
/// This specifically validates:
/// * `iat` (issued at)
/// * `exp` (expired)
/// * `nbf` (not before)
///
/// This specifically does not validate:
/// * `audience`
/// * `jti`
/// * `issuedBy`
/// * `subject`
///
/// Because we validate these fields the resulting type must be a json object. If it's not
/// please use the protocol impls directly.
///
/// # Errors
///
/// 1. If the token cannot be decrypted.
/// 2. If any of the token fields are invalid: `iat`, `exp`, or `nbf`.
pub fn validate_public_token(
token: &str,
footer: Option<&str>,
key: &PasetoPublicKey,
backend: &TimeBackend,
) -> Result<JsonValue, PasetoError> {
#[cfg(feature = "v2")]
{
if token.starts_with("v2.public.") {
return match key {
PasetoPublicKey::ED25519KeyPair(key_pair) => {
let internal_msg = V2Verify(token, footer, key_pair.public_key().as_ref())?;
validate_potential_json_blob(&internal_msg, backend)
}
PasetoPublicKey::ED25519PublicKey(pub_key_contents) => {
let internal_msg = V2Verify(token, footer, pub_key_contents)?;
validate_potential_json_blob(&internal_msg, backend)
}
#[cfg(feature = "v1")]
_ => Err(GenericError::NoKeyProvided {}.into()),
};
}
}
#[cfg(feature = "v1")]
{
if token.starts_with("v1.public.") {
return match key {
PasetoPublicKey::RSAPublicKey(key_content) => {
let internal_msg = V1Verify(token, footer, key_content)?;
validate_potential_json_blob(&internal_msg, backend)
}
#[cfg(feature = "v2")]
_ => Err(GenericError::NoKeyProvided {}.into()),
};
}
}
Err(GenericError::InvalidToken {}.into())
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[cfg(feature = "easy_tokens_chrono")]
use chrono::Duration;
#[cfg(feature = "v2")]
use ring::rand::SystemRandom;
use serde_json::json;
#[test]
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
fn valid_enc_token_passes_test() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() + 1, 7, 8)
.and_hms(9, 10, 11);
let token = PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&Utc::now())
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
validate_local_token(
&token,
Some("footer"),
&"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
&TimeBackend::Chrono,
)
.expect("Failed to validate token!");
}
#[test]
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
fn valid_enc_token_expired_test() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() - 1, 7, 8)
.and_hms(9, 10, 11);
let token = PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&Utc::now())
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
let _error: PasetoError = (GenericError::ExpiredToken {}).into();
assert!(matches!(
validate_local_token(
&token,
Some("footer"),
&"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
&TimeBackend::Chrono
),
Err(_error)
));
}
#[test]
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
fn valid_enc_token_not_before_test() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() - 1, 7, 8)
.and_hms(9, 10, 11);
let token = PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&(Utc::now() + Duration::days(1)))
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
let _error: PasetoError = (GenericError::InvalidNotBeforeToken {}).into();
assert!(matches!(
validate_local_token(
&token,
Some("footer"),
&"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
&TimeBackend::Chrono
),
Err(_error)
));
}
#[test]
#[cfg(all(feature = "easy_tokens_chrono", not(feature = "easy_tokens_time")))]
fn invalid_enc_token_doesnt_validate() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() - 1, 7, 8)
.and_hms(9, 10, 11);
let token = PasetoBuilder::new()
.set_encryption_key(&Vec::from("YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes()))
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&Utc::now())
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
assert!(validate_local_token(
&token,
Some("footer"),
&"YELLOW SUBMARINE, BLACK WIZARDRY".as_bytes(),
&TimeBackend::Chrono
)
.is_err());
}
#[test]
#[cfg(all(
feature = "v2",
feature = "easy_tokens_chrono",
not(feature = "easy_tokens_time")
))]
fn valid_pub_token_passes_test() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() + 1, 7, 8)
.and_hms(9, 10, 11);
let sys_rand = SystemRandom::new();
let key_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!");
let as_key =
Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair");
let token = PasetoBuilder::new()
.set_ed25519_key(&as_key)
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&Utc::now())
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
validate_public_token(
&token,
Some("footer"),
&PasetoPublicKey::ED25519KeyPair(&as_key),
&TimeBackend::Chrono,
)
.expect("Failed to validate token!");
}
#[test]
#[cfg(all(
feature = "v2",
feature = "easy_tokens_chrono",
not(feature = "easy_tokens_time")
))]
fn validate_pub_key_only_v2() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() + 1, 7, 8)
.and_hms(9, 10, 11);
let sys_rand = SystemRandom::new();
let key_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!");
let as_key =
Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair");
let token = PasetoBuilder::new()
.set_ed25519_key(&as_key)
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&Utc::now())
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
validate_public_token(
&token,
Some("footer"),
&PasetoPublicKey::ED25519PublicKey(as_key.public_key().as_ref()),
&TimeBackend::Chrono,
)
.expect("Failed to validate token!");
}
#[test]
#[cfg(all(
feature = "v2",
feature = "easy_tokens_chrono",
not(feature = "easy_tokens_time")
))]
fn invalid_pub_token_doesnt_validate() {
let current_date_time = Utc::now();
let dt = Utc
.ymd(current_date_time.year() - 1, 7, 8)
.and_hms(9, 10, 11);
let sys_rand = SystemRandom::new();
let key_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!");
let as_key =
Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair");
let token = PasetoBuilder::new()
.set_ed25519_key(&as_key)
.set_issued_at(None)
.set_expiration(&dt)
.set_issuer("issuer")
.set_audience("audience")
.set_jti("jti")
.set_not_before(&Utc::now())
.set_subject("test")
.set_claim("claim", json!("data"))
.set_footer("footer")
.build()
.expect("Failed to construct paseto token w/ builder!");
assert!(validate_public_token(
&token,
Some("footer"),
&PasetoPublicKey::ED25519KeyPair(&as_key),
&TimeBackend::Chrono
)
.is_err());
}
#[test]
#[cfg(all(
feature = "v2",
feature = "easy_tokens_chrono",
not(feature = "easy_tokens_time")
))]
fn allows_validation_without_iat_exp_nbf() {
let key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let state = PasetoBuilder::new()
.set_encryption_key(key.as_bytes())
.build()
.expect("failed to construct paseto token");
assert!(
validate_local_token(&state, None, key.as_bytes(), &TimeBackend::Chrono).is_ok(),
"Failed to validate token without nbf/iat/exp is okay!"
);
}
}
================================================
FILE: src/v1/local.rs
================================================
//! ["Direct" use of local (symmetrically-encrypted) tokens for V1 of Paseto.](https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b058649d01a82a8c412087bc87/docs/01-Protocol-Versions/Version1.md#encrypt)
use crate::{
errors::{GenericError, PasetoError, SodiumErrors},
pae::pae,
};
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use openssl::symm;
use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals;
use ring::hkdf::{Salt, HKDF_SHA384};
use ring::hmac::{sign, Key, HMAC_SHA384};
use ring::rand::{SecureRandom, SystemRandom};
const HEADER: &str = "v1.local.";
/// Encrypt a paseto token using `v1` of Paseto.
///
/// Keys must be exactly 32 bytes long, this is a requirement of the underlying
/// algorithim. Returns a result of the token as a string if encryption was successful.
///
/// # Errors
///
/// - If you pass in a key that is not exactly 32 bytes in length.
/// - If we fail to talk to the system random number generator to generate 32 bytes.
/// - If the calls to openssl to encrypt your data fails.
pub fn local_paseto(msg: &str, footer: Option<&str>, key: &[u8]) -> Result<String, PasetoError> {
if key.len() != 32 {
return Err(SodiumErrors::InvalidKeySize {
size_needed: 32,
size_provided: key.len(),
}
.into());
}
let rng = SystemRandom::new();
let mut buff: [u8; 32] = [0_u8; 32];
let res = rng.fill(&mut buff);
if res.is_err() {
return Err(PasetoError::GenericError(GenericError::RandomError {}));
}
underlying_local_paseto(msg, footer, &buff, key)
}
/// Performs the underlying encryption of a paseto token. Split for ease in unit testing.
fn underlying_local_paseto(
msg: &str,
footer: Option<&str>,
random_nonce: &[u8],
key: &[u8],
) -> Result<String, PasetoError> {
let footer_frd = footer.unwrap_or("");
let true_nonce = calculate_hashed_nonce(msg.as_bytes(), random_nonce);
let (as_salt, ctr_nonce) = true_nonce.split_at(16);
let hkdf_salt = Salt::new(HKDF_SHA384, as_salt);
let mut ek = [0; 32];
let mut ak = [0; 32];
let encryptionkey_info = ["paseto-encryption-key".as_bytes()];
let authkey_info = ["paseto-auth-key-for-aead".as_bytes()];
let extracted = hkdf_salt.extract(key);
let encryptionkey_wrapped_res = extracted.expand(&encryptionkey_info, CustomKeyWrapper(32));
let authkey_wrapped_res = extracted.expand(&authkey_info, CustomKeyWrapper(32));
if encryptionkey_wrapped_res.is_err() || authkey_wrapped_res.is_err() {
return Err(GenericError::BadHkdf {}.into());
}
let encryptionkey_fill_res = encryptionkey_wrapped_res.unwrap().fill(&mut ek);
let accesskey_fill_res = authkey_wrapped_res.unwrap().fill(&mut ak);
if encryptionkey_fill_res.is_err() || accesskey_fill_res.is_err() {
return Err(GenericError::BadHkdf {}.into());
}
let cipher = symm::Cipher::aes_256_ctr();
let crypted = symm::encrypt(cipher, &ek, Some(ctr_nonce), msg.as_bytes())?;
let pre_auth = pae(&[
HEADER.as_bytes(),
&true_nonce,
&crypted,
footer_frd.as_bytes(),
]);
let mac_key = Key::new(HMAC_SHA384, &ak);
let signed = sign(&mac_key, &pre_auth);
let raw_bytes_from_hmac = signed.as_ref();
let mut concated_together = Vec::new();
concated_together.extend_from_slice(&true_nonce);
concated_together.extend_from_slice(&crypted);
concated_together.extend_from_slice(raw_bytes_from_hmac);
let token = if footer_frd.is_empty() {
format!(
"{}{}",
HEADER,
encode_config(&concated_together, URL_SAFE_NO_PAD)
)
} else {
format!(
"{}{}.{}",
HEADER,
encode_config(&concated_together, URL_SAFE_NO_PAD),
encode_config(footer_frd.as_bytes(), URL_SAFE_NO_PAD)
)
};
Ok(token)
}
/// An implementation of `get_nonce` from the docs in paseto version one.
///
/// This function is to ensure that an RNG failure does not result in a
/// nonce-misuse condition that breaks the security of our stream cipher.
#[must_use]
fn calculate_hashed_nonce(msg: &[u8], random_nonce: &[u8]) -> Vec<u8> {
let mac_key = Key::new(HMAC_SHA384, random_nonce);
let signed = sign(&mac_key, msg);
Vec::from(&signed.as_ref()[0..32])
}
/// A small module containing a simple structure that allows us to implement
/// hkdf on any type regardless if we own it or not.
///
/// BORROWED FROM RING Itself.
/// LICENSE: <https://github.com/briansmith/ring/blob/master/LICENSE>
struct CustomKeyWrapper<T>(pub T);
impl ring::hkdf::KeyType for CustomKeyWrapper<usize> {
fn len(&self) -> usize {
self.0
}
}
impl From<ring::hkdf::Okm<'_, CustomKeyWrapper<usize>>> for CustomKeyWrapper<Vec<u8>> {
fn from(okm: ring::hkdf::Okm<CustomKeyWrapper<usize>>) -> Self {
let mut r = vec![0_u8; okm.len().0];
okm.fill(&mut r).unwrap();
CustomKeyWrapper(r)
}
}
/// Decrypt a paseto token using `v1` of Paseto, validating the footer.
///
/// Returns the contents of the token as a string.
///
/// # Errors
///
/// - If the token is not in the proper format: `v1.local.${encrypted_data}(.{optional_footer})?`
/// - If the footer on the token did not match the footer passed in.
/// - If we failed to decrypt the data.
/// - If the data contained in the token was not valid utf-8.
pub fn decrypt_paseto(
token: &str,
footer: Option<&str>,
key: &[u8],
) -> Result<String, PasetoError> {
let token_parts = token.split('.').collect::<Vec<_>>();
if token_parts.len() < 3 {
return Err(GenericError::InvalidToken {}.into());
}
let is_footer_some = footer.is_some();
let footer_str = footer.unwrap_or("");
if is_footer_some {
if token_parts.len() < 4 {
return Err(GenericError::InvalidFooter {}.into());
}
let as_base64 = encode_config(footer_str.as_bytes(), URL_SAFE_NO_PAD);
if ConstantTimeEquals(as_base64.as_bytes(), token_parts[3].as_bytes()).is_err() {
return Err(GenericError::InvalidFooter {}.into());
}
}
if token_parts[0] != "v1" || token_parts[1] != "local" {
return Err(GenericError::InvalidToken {}.into());
}
let decoded = decode_config(token_parts[2].as_bytes(), URL_SAFE_NO_PAD)
.map_err(|e| PasetoError::GenericError(GenericError::Base64Error(e)))?;
let (nonce, t_and_c) = decoded.split_at(32);
// NLL :shakefists:
let t_and_c_vec = Vec::from(t_and_c);
let t_and_c_len = t_and_c_vec.len();
let (ciphertext, mac) = t_and_c_vec.split_at(t_and_c_len - 48);
let nonce = Vec::from(nonce);
let (as_salt, ctr_nonce) = nonce.split_at(16);
let hkdf_salt = Salt::new(HKDF_SHA384, as_salt);
let mut ek = [0; 32];
let mut ak = [0; 32];
let extracted = hkdf_salt.extract(key);
let encryptionkey_info = ["paseto-encryption-key".as_bytes()];
let authkey_info = ["paseto-auth-key-for-aead".as_bytes()];
let encryptionkey_wrapped_res = extracted
.expand(&encryptionkey_info, CustomKeyWrapper(32))
.map_err(|_| GenericError::BadHkdf {})?;
let authkey_wrapped_res = extracted
.expand(&authkey_info, CustomKeyWrapper(32))
.map_err(|_| GenericError::BadHkdf {})?;
encryptionkey_wrapped_res
.fill(&mut ek)
.map_err(|_| GenericError::BadHkdf {})?;
authkey_wrapped_res
.fill(&mut ak)
.map_err(|_| GenericError::BadHkdf {})?;
let pre_auth = pae(&[HEADER.as_bytes(), &nonce, ciphertext, footer_str.as_bytes()]);
let mac_key = Key::new(HMAC_SHA384, &ak);
let signed = sign(&mac_key, &pre_auth);
let raw_bytes_from_hmac = signed.as_ref();
if ConstantTimeEquals(raw_bytes_from_hmac, mac).is_err() {
return Err(GenericError::InvalidToken {}.into());
}
let cipher = symm::Cipher::aes_256_ctr();
let decrypted = symm::decrypt(cipher, &ek, Some(ctr_nonce), ciphertext)?;
String::from_utf8(decrypted).map_err(|e| PasetoError::GenericError(GenericError::Utf8Error(e)))
}
#[cfg(test)]
mod unit_tests {
use super::*;
use hex;
use ring::rand::{SecureRandom, SystemRandom};
#[test]
fn test_v1_local() {
let rng = SystemRandom::new();
let mut key_buff: [u8; 32] = [0_u8; 32];
rng.fill(&mut key_buff).expect("Failed to fill key_buff!");
// Try to encrypt without footers.
let message_a =
local_paseto("msg", None, &key_buff).expect("Failed to encrypt V1 Paseto string");
// NOTE: This test is just ensuring we can encode a json object, remember these internal impls
// don't check for expires being valid!
let message_b = local_paseto(
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}",
None,
&key_buff,
)
.expect("Failed to encrypt V1 Paseto Json BLOB");
assert!(message_a.starts_with("v1.local."));
assert!(message_b.starts_with("v1.local."));
let decrypted_a = decrypt_paseto(&message_a, None, &key_buff)
.expect("Failed to decrypt V1 Paseto String");
let decrypted_b = decrypt_paseto(&message_b, None, &key_buff)
.expect("Failed to decrypt V1 Paseto JSON Blob");
assert_eq!(decrypted_a, "msg");
assert_eq!(
decrypted_b,
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}"
);
let should_fail_decryption_a = decrypt_paseto(&message_a, Some("data"), &key_buff);
assert!(should_fail_decryption_a.is_err());
// Try with footers.
let message_c = local_paseto("msg", Some("data"), &key_buff)
.expect("Failed to encrypt V1 Paseto String with footer!");
let message_d = local_paseto(
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}",
Some("data"),
&key_buff,
)
.expect("Failed to encrypt V1 Paseto Json blob with footer!");
assert!(message_c.starts_with("v1.local."));
assert!(message_d.starts_with("v1.local."));
let decrypted_c = decrypt_paseto(&message_c, Some("data"), &key_buff)
.expect("Failed to decrypt V1 Paseto String with footer!");
let decrypted_d = decrypt_paseto(&message_d, Some("data"), &key_buff)
.expect("Failed to decrypt V1 Paseto Json Blob with footer!");
assert_eq!(decrypted_c, "msg");
assert_eq!(
decrypted_d,
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}"
);
// Try with no footer + invalid footer.
let should_fail_decryption_b = decrypt_paseto(&message_c, None, &key_buff);
let should_fail_decryption_c = decrypt_paseto(&message_c, Some("invalid"), &key_buff);
assert!(should_fail_decryption_b.is_err());
assert!(should_fail_decryption_c.is_err());
}
#[test]
fn test_nonce_derivation() {
// Constants copied directly from paseto source.
let msg_a = "The quick brown fox jumped over the lazy dog.";
let msg_b = "The quick brown fox jumped over the lazy dof.";
let nonce =
hex::decode("808182838485868788898a8b8c8d8e8f").expect("Failed to decode nonce!");
let calculated_nonce_a = calculate_hashed_nonce(msg_a.as_bytes(), &nonce);
let calculated_nonce_b = calculate_hashed_nonce(msg_b.as_bytes(), &nonce);
assert_eq!(
"5e13b4f0fc111bf0cf9de4e97310b687858b51547e125790513cc1eaaef173cc",
hex::encode(&calculated_nonce_a)
);
assert_eq!(
"e1ba992f5cccd31714fd8c73adcdadabb00d0f23955a66907170c10072d66ffd",
hex::encode(&calculated_nonce_b)
)
}
}
================================================
FILE: src/v1/mod.rs
================================================
//! ["Direct" use of Protocol for V1 of Paseto.](https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b058649d01a82a8c412087bc87/docs/01-Protocol-Versions/Version1.md)
//!
//! It is recommended to use the easy tokens, which are present in the
//! [`crate::tokens`] module. As it automatically can validate things like
//! expirey time, however the direct protocol allows you to encode arbitrary
//! data which can be beneficial.
//!
//! Local Encryption Methods:
//! - [`self::local_paseto`] / [`self::decrypt_paseto`]
//!
//! Public Signing Methods:
//! - [`self::public_paseto`] / [`self::verify_paseto`]
pub mod local;
pub mod public;
pub use self::local::*;
pub use self::public::*;
================================================
FILE: src/v1/public.rs
================================================
//! ["Direct" use of public (signed but readable) tokens for V1 of Paseto.](https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b058649d01a82a8c412087bc87/docs/01-Protocol-Versions/Version1.md#sign)
use crate::{
errors::{GenericError, PasetoError, RsaKeyErrors},
pae::pae,
};
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals;
use ring::rand::SystemRandom;
use ring::signature::{RsaKeyPair, UnparsedPublicKey, RSA_PSS_2048_8192_SHA384, RSA_PSS_SHA384};
const HEADER: &str = "v1.public.";
/// Sign a paseto token using `v1` of Paseto.
///
/// Returns a result of the token as a string if encryption was successful.
///
/// # Errors
///
/// - If the RSA Public Modulus Len is not 256 (you pass an invalid RSA Key).
/// - If we fail to talk to the system random number generator to generate random numbers.
/// - If we fail to call openssl to sign your data.
pub fn public_paseto(
msg: &str,
footer: Option<&str>,
key_pair: &RsaKeyPair,
) -> Result<String, PasetoError> {
if key_pair.public_modulus_len() != 256 {
return Err(RsaKeyErrors::InvalidKey {}.into());
}
let footer_frd = footer.unwrap_or("");
let pre_auth = pae(&[HEADER.as_bytes(), msg.as_bytes(), footer_frd.as_bytes()]);
let random = SystemRandom::new();
let mut signed_msg = [0; 256];
let sign_res = key_pair.sign(&RSA_PSS_SHA384, &random, &pre_auth, &mut signed_msg);
if sign_res.is_err() {
return Err(RsaKeyErrors::SignError {}.into());
}
let mut combined_vec = Vec::new();
combined_vec.extend_from_slice(msg.as_bytes());
combined_vec.extend_from_slice(&signed_msg);
let token = if footer_frd.is_empty() {
format!(
"{}{}",
HEADER,
encode_config(&combined_vec, URL_SAFE_NO_PAD)
)
} else {
format!(
"{}{}.{}",
HEADER,
encode_config(&combined_vec, URL_SAFE_NO_PAD),
encode_config(footer_frd.as_bytes(), URL_SAFE_NO_PAD)
)
};
Ok(token)
}
/// Verifies the signature of a paseto token using `v1` of Paseto, validating the footer.
///
/// Returns the contents of the token as a string.
///
/// # Errors
///
/// - If the token is not in the proper format: `v1.public.${signed_encoded_data}(.{optional_footer})?`
/// - If the footer on the token did not match the footer passed in.
/// - If we failed to validate the signature of the data.
/// - If the data contained in the token was not valid utf-8.
pub fn verify_paseto(
token: &str,
footer: Option<&str>,
public_key: &[u8],
) -> Result<String, PasetoError> {
let token_parts = token.split('.').collect::<Vec<_>>();
if token_parts.len() < 3 {
return Err(GenericError::InvalidToken {}.into());
}
let has_provided_footer = footer.is_some();
let footer_as_str = footer.unwrap_or("");
if has_provided_footer {
if token_parts.len() < 4 {
return Err(GenericError::InvalidFooter {}.into());
}
let footer_encoded = encode_config(footer_as_str.as_bytes(), URL_SAFE_NO_PAD);
if ConstantTimeEquals(footer_encoded.as_bytes(), token_parts[3].as_bytes()).is_err() {
return Err(GenericError::InvalidFooter {}.into());
}
}
if token_parts[0] != "v1" || token_parts[1] != "public" {
return Err(GenericError::InvalidToken {}.into());
}
let decoded = decode_config(token_parts[2].as_bytes(), URL_SAFE_NO_PAD)
.map_err(|e| PasetoError::GenericError(GenericError::Base64Error(e)))?;
let decoded_len = decoded.len();
let (message, sig) = decoded.split_at(decoded_len - 256);
let pre_auth = pae(&[HEADER.as_bytes(), message, footer_as_str.as_bytes()]);
let pk_unparsed = UnparsedPublicKey::new(&RSA_PSS_2048_8192_SHA384, public_key);
let verify_result = pk_unparsed.verify(&pre_auth, sig);
if verify_result.is_err() {
return Err(GenericError::InvalidToken {}.into());
}
String::from_utf8(Vec::from(message))
.map_err(|e| PasetoError::GenericError(GenericError::Utf8Error(e)))
}
#[cfg(test)]
mod unit_tests {
use super::*;
use ring::signature::RsaKeyPair;
#[test]
fn test_v1_public() {
let private_key = include_bytes!("signature_rsa_example_private_key.der");
let public_key = include_bytes!("signature_rsa_example_public_key.der");
let key_pair = RsaKeyPair::from_der(private_key).expect("Bad Private Key pkcs!");
// Test keys without footers.
let public_token_one = public_paseto("msg", None, &key_pair)
.expect("Failed to encode public paseto v1 msg with no footer!");
// Remember raw protocol doesn't validate expires. We're just ensuring we can encode it.
let public_token_two = public_paseto(
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}",
None,
&key_pair,
)
.expect("Failed to encode public paseto v1 json blob with no footer!");
let verified_one = verify_paseto(&public_token_one, None, public_key)
.expect("Failed to verify public paseto v1 msg with no footer!");
let verified_two = verify_paseto(&public_token_two, None, public_key)
.expect("Failed to verify public paseto v1 json blob with no footer!");
assert_eq!(verified_one, "msg");
assert_eq!(
verified_two,
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}"
);
// Attempt to verify with a footer
let should_not_verify_one = verify_paseto(&public_token_one, Some("hoi"), public_key);
assert!(should_not_verify_one.is_err());
let public_token_three = public_paseto("msg", Some("data"), &key_pair)
.expect("Failed to encode public paseto v1 msg with footer!");
let public_token_four = public_paseto(
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}",
Some("data"),
&key_pair,
)
.expect("Failed to encode public paseto v1 json blob with footer!");
let verified_three = verify_paseto(&public_token_three, Some("data"), public_key)
.expect("Failed to verify public paseto v1 msg with footer!");
let verified_four = verify_paseto(&public_token_four, Some("data"), public_key)
.expect("Failed to verify public paseto v1 json blob with footer!");
assert_eq!(verified_three, "msg");
assert_eq!(
verified_four,
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}"
);
// Ensure that no footer + incorrect footer fail to validate.
let should_not_verify_two = verify_paseto(&public_token_three, None, public_key);
let should_not_verify_three = verify_paseto(&public_token_three, Some("test"), public_key);
assert!(should_not_verify_two.is_err());
assert!(should_not_verify_three.is_err());
}
}
================================================
FILE: src/v2/local.rs
================================================
//! ["Direct" use of local (symmetrically-encrypted) tokens for V2 of Paseto.](https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b058649d01a82a8c412087bc87/docs/01-Protocol-Versions/Version2.md#encrypt)
use crate::{
errors::{GenericError, PasetoError, SodiumErrors},
pae::pae,
};
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use blake2::{
digest::{Update, VariableOutput},
VarBlake2b,
};
use chacha20poly1305::{
aead::{Aead, NewAead, Payload},
XChaCha20Poly1305, XNonce,
};
use ring::{
constant_time::verify_slices_are_equal as ConstantTimeEquals,
rand::{SecureRandom, SystemRandom},
};
const HEADER: &str = "v2.local.";
/// Encrypt a paseto token using `v2` of Paseto.
///
/// Keys must be exactly 32 bytes long, this is a requirement of the underlying
/// algorithim. Returns a result of the token as a string if encryption was successful.
///
/// # Errors
///
/// - If you pass in a key that is not exactly 32 bytes in length.
/// - If we fail to talk to the system random number generator to generate 24 bytes.
/// - If the calls to libsodium to encrypt your data fails.
pub fn local_paseto(msg: &str, footer: Option<&str>, key: &[u8]) -> Result<String, PasetoError> {
if key.len() != 32 {
return Err(SodiumErrors::InvalidKeySize {
size_needed: 32,
size_provided: key.len(),
}
.into());
}
let rng = SystemRandom::new();
let mut buff: [u8; 24] = [0_u8; 24];
let res = rng.fill(&mut buff);
if res.is_err() {
return Err(GenericError::RandomError {}.into());
}
underlying_local_paseto(msg, footer, &buff, key)
}
/// Performs the underlying encryption of a paseto token. Split for ease in unit testing.
fn underlying_local_paseto(
msg: &str,
footer: Option<&str>,
nonce_key: &[u8; 24],
key: &[u8],
) -> Result<String, PasetoError> {
let footer_frd = footer.unwrap_or("");
let mut state = VarBlake2b::new_keyed(nonce_key, 24);
state.update(msg.as_bytes());
let finalized = state.finalize_boxed();
let nonce = XNonce::from_slice(finalized.as_ref());
if let Ok(aead) = XChaCha20Poly1305::new_from_slice(key) {
let pre_auth = pae(&[HEADER.as_bytes(), finalized.as_ref(), footer_frd.as_bytes()]);
if let Ok(crypted) = aead.encrypt(
nonce,
Payload {
msg: msg.as_bytes(),
aad: pre_auth.as_ref(),
},
) {
let mut n_and_c = Vec::new();
n_and_c.extend_from_slice(finalized.as_ref());
n_and_c.extend_from_slice(crypted.as_ref());
let token = if footer_frd.is_empty() {
format!("{}{}", HEADER, encode_config(&n_and_c, URL_SAFE_NO_PAD))
} else {
format!(
"{}{}.{}",
HEADER,
encode_config(&n_and_c, URL_SAFE_NO_PAD),
encode_config(footer_frd.as_bytes(), URL_SAFE_NO_PAD)
)
};
Ok(token)
} else {
Err(SodiumErrors::FunctionError {}.into())
}
} else {
Err(SodiumErrors::InvalidKeySize {
size_provided: key.len(),
size_needed: 32,
}
.into())
}
}
/// Decrypt a paseto token using `v2` of Paseto, validating the footer.
///
/// Returns the contents of the token as a string.
///
/// # Errors
///
/// - If the token is not in the proper format: `v2.local.${encrypted_data}(.{optional_footer})?`
/// - If the footer on the token did not match the footer passed in.
/// - If we failed to decrypt the data.
/// - If the data contained in the token was not valid utf-8.
pub fn decrypt_paseto(
token: &str,
footer: Option<&str>,
key: &[u8],
) -> Result<String, PasetoError> {
let token_parts = token.split('.').collect::<Vec<_>>();
if token_parts.len() < 3 {
return Err(GenericError::InvalidToken {}.into());
}
let is_footer_some = footer.is_some();
let footer_str = footer.unwrap_or("");
if is_footer_some {
if token_parts.len() < 4 {
return Err(GenericError::InvalidFooter {}.into());
}
let as_base64 = encode_config(footer_str.as_bytes(), URL_SAFE_NO_PAD);
if ConstantTimeEquals(as_base64.as_bytes(), token_parts[3].as_bytes()).is_err() {
return Err(GenericError::InvalidFooter {}.into());
}
}
if token_parts[0] != "v2" || token_parts[1] != "local" {
return Err(GenericError::InvalidToken {}.into());
}
let mut decoded = decode_config(token_parts[2].as_bytes(), URL_SAFE_NO_PAD)
.map_err(|e| PasetoError::GenericError(GenericError::Base64Error(e)))?;
let (nonce, ciphertext) = decoded.split_at_mut(24);
let pre_auth = pae(&[HEADER.as_bytes(), nonce, footer_str.as_bytes()]);
let nonce_obj = XNonce::from_slice(nonce);
let aead = XChaCha20Poly1305::new_from_slice(key)
.map_err(|_| PasetoError::LibsodiumError(SodiumErrors::InvalidKey {}))?;
match aead.decrypt(
nonce_obj,
Payload {
msg: ciphertext,
aad: &pre_auth,
},
) {
Ok(decrypted) => String::from_utf8(decrypted)
.map_err(|e| PasetoError::GenericError(GenericError::Utf8Error(e))),
Err(_) => Err(SodiumErrors::FunctionError {}.into()),
}
}
#[cfg(test)]
mod unit_tests {
use super::*;
#[test]
fn paseto_empty_encrypt_verify() {
let empty_key = [0; 32];
let full_key = [255; 32];
let result = underlying_local_paseto("", None, &[0; 24], &empty_key);
if result.is_err() {
println!("Failed to encrypt Paseto!");
println!("{:?}", result);
panic!("Paseto Failure Encryption!");
}
let the_str = result.unwrap();
assert_eq!(
"v2.local.driRNhM20GQPvlWfJCepzh6HdijAq-yNUtKpdy5KXjKfpSKrOlqQvQ",
the_str
);
let result_full = underlying_local_paseto("", None, &[0; 24], &full_key);
if result_full.is_err() {
println!("Failed to encrypt Paseto!");
println!("{:?}", result_full);
panic!("Paseto Failure Encryption!");
}
let the_full_str = result_full.unwrap();
assert_eq!(
"v2.local.driRNhM20GQPvlWfJCepzh6HdijAq-yNSOvpveyCsjPYfe9mtiJDVg",
the_full_str
);
}
#[test]
fn paseto_non_empty_footer_encrypt_verify() {
let empty_key = [0; 32];
let full_key = [255; 32];
let result = underlying_local_paseto("", Some("Cuon Alpinus"), &[0; 24], &empty_key);
if result.is_err() {
println!("Failed to encrypt Paseto!");
println!("{:?}", result);
panic!("Paseto Failure Encryption!");
}
let the_str = result.unwrap();
assert_eq!(
"v2.local.driRNhM20GQPvlWfJCepzh6HdijAq-yNfzz6yGkE4ZxojJAJwKLfvg.Q3VvbiBBbHBpbnVz",
the_str
);
let full_result = underlying_local_paseto("", Some("Cuon Alpinus"), &[0; 24], &full_key);
if full_result.is_err() {
println!("Failed to encrypt Paseto!");
println!("{:?}", full_result);
panic!("Paseto Failure Encryption!");
}
let full_str = full_result.unwrap();
assert_eq!(
"v2.local.driRNhM20GQPvlWfJCepzh6HdijAq-yNJbTJxAGtEg4ZMXY9g2LSoQ.Q3VvbiBBbHBpbnVz",
full_str
);
}
#[test]
fn paseto_non_empty_msg_encrypt_verify() {
let empty_key = [0; 32];
let full_key = [255; 32];
let result = underlying_local_paseto(
"Love is stronger than hate or fear",
None,
&[0; 24],
&empty_key,
);
if result.is_err() {
println!("Failed to encrypt Paseto!");
println!("{:?}", result);
panic!("Paseto Failure Encryption!");
}
let the_str = result.unwrap();
assert_eq!(
"v2.local.BEsKs5AolRYDb_O-bO-lwHWUextpShFSvu6cB-KuR4wR9uDMjd45cPiOF0zxb7rrtOB5tRcS7dWsFwY4ONEuL5sWeunqHC9jxU0",
the_str
);
let full_result = underlying_local_paseto(
"Love is stronger than hate or fear",
None,
&[0; 24],
&full_key,
);
if full_result.is_err() {
println!("Failed to encrypt Paseto!");
println!("{:?}", full_result);
panic!("Paseto Failure Encryption!");
}
let full_str = full_result.unwrap();
assert_eq!(
"v2.local.BEsKs5AolRYDb_O-bO-lwHWUextpShFSjvSia2-chHyMi4LtHA8yFr1V7iZmKBWqzg5geEyNAAaD6xSEfxoET1xXqahe1jqmmPw",
full_str
);
}
#[test]
fn full_round_paseto() {
let empty_key = [0; 32];
let result = local_paseto(
"Love is stronger than hate or fear",
Some("gwiz-bot"),
&empty_key,
);
if result.is_err() {
println!("Failed to encrypt Paseto!");
println!("{:?}", result);
panic!("Paseto Failure Encryption!");
}
let the_str = result.unwrap();
println!("Paseto Full Round Token: [ {:?} ]", the_str);
let decrypted_result = decrypt_paseto(&the_str, Some("gwiz-bot"), &empty_key);
if decrypted_result.is_err() {
println!("Failed to decrypt Paseto!");
println!("{:?}", decrypted_result);
panic!("Paseto Failure Decryption!");
}
let decrypted = decrypted_result.unwrap();
assert_eq!(decrypted, "Love is stronger than hate or fear");
}
}
================================================
FILE: src/v2/mod.rs
================================================
//! ["Direct" use of Protocol for V2 of Paseto.](https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b058649d01a82a8c412087bc87/docs/01-Protocol-Versions/Version2.md)
//!
//! It is recommended to use the easy tokens, which are present in the
//! [`crate::tokens`] module. As it automatically can validate things like
//! expirey time, however the direct protocol allows you to encode arbitrary
//! data which can be beneficial.
//!
//! Local Encryption Methods:
//! - [`self::local_paseto`] / [`self::decrypt_paseto`]
//!
//! Public Signing Methods:
//! - [`self::public_paseto`] / [`self::verify_paseto`]
pub mod local;
pub mod public;
pub use self::local::*;
pub use self::public::*;
================================================
FILE: src/v2/public.rs
================================================
//! ["Direct" use of public (signed but readable) tokens for V2 of Paseto.](https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b058649d01a82a8c412087bc87/docs/01-Protocol-Versions/Version2.md#sign)
use crate::{
errors::{GenericError, PasetoError},
pae::pae,
};
use base64::{decode_config, encode_config, URL_SAFE_NO_PAD};
use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals;
use ring::signature::{Ed25519KeyPair, UnparsedPublicKey, ED25519};
const HEADER: &str = "v2.public.";
/// Sign a paseto token using `v2` of Paseto.
///
/// Returns a result of the token as a string if encryption was successful.
///
/// # Errors
///
/// - If the calls to libsodium to sign your data fails.
pub fn public_paseto(
msg: &str,
footer: Option<&str>,
key_pair: &Ed25519KeyPair,
) -> Result<String, PasetoError> {
let footer_frd = footer.unwrap_or("");
let pre_auth = pae(&[HEADER.as_bytes(), msg.as_bytes(), footer_frd.as_bytes()]);
let sig = key_pair.sign(&pre_auth);
let mut m_and_sig = Vec::from(msg.as_bytes());
m_and_sig.extend_from_slice(sig.as_ref());
let token = if footer_frd.is_empty() {
format!("{}{}", HEADER, encode_config(&m_and_sig, URL_SAFE_NO_PAD))
} else {
format!(
"{}{}.{}",
HEADER,
encode_config(&m_and_sig, URL_SAFE_NO_PAD),
encode_config(footer_frd.as_bytes(), URL_SAFE_NO_PAD)
)
};
Ok(token)
}
/// Verifies the signature of a paseto token using `v2` of Paseto, validating the footer.
///
/// Returns the contents of the token as a string.
///
/// # Errors
///
/// - If the token is not in the proper format: `v2.public.${signed_encoded_data}(.{optional_footer})?`
/// - If the footer on the token did not match the footer passed in.
/// - If we failed to validate the signature of the data.
/// - If the data contained in the token was not valid utf-8.
pub fn verify_paseto(
token: &str,
footer: Option<&str>,
public_key: &[u8],
) -> Result<String, PasetoError> {
let token_parts = token.split('.').collect::<Vec<_>>();
if token_parts.len() < 3 {
return Err(PasetoError::GenericError(GenericError::InvalidToken {}));
}
let has_provided_footer = footer.is_some();
let footer_as_str = footer.unwrap_or("");
if has_provided_footer {
if token_parts.len() < 4 {
return Err(PasetoError::GenericError(GenericError::InvalidFooter {}));
}
let footer_encoded = encode_config(footer_as_str.as_bytes(), URL_SAFE_NO_PAD);
if ConstantTimeEquals(footer_encoded.as_bytes(), token_parts[3].as_bytes()).is_err() {
return Err(PasetoError::GenericError(GenericError::InvalidFooter {}));
}
}
if token_parts[0] != "v2" || token_parts[1] != "public" {
return Err(PasetoError::GenericError(GenericError::InvalidToken {}));
}
let decoded = decode_config(token_parts[2].as_bytes(), URL_SAFE_NO_PAD)
.map_err(|e| PasetoError::GenericError(GenericError::Base64Error(e)))?;
let decoded_len = decoded.len();
let (msg, sig) = decoded.split_at(decoded_len - 64);
let pre_auth = pae(&[HEADER.as_bytes(), msg, footer_as_str.as_bytes()]);
let pk_unparsed = UnparsedPublicKey::new(&ED25519, public_key);
let verify_res = pk_unparsed.verify(&pre_auth, sig);
if verify_res.is_err() {
return Err(PasetoError::GenericError(GenericError::InvalidToken {}));
}
String::from_utf8(Vec::from(msg))
.map_err(|e| PasetoError::GenericError(GenericError::Utf8Error(e)))
}
#[cfg(test)]
mod unit_tests {
use super::*;
use ring::rand::SystemRandom;
use ring::signature::KeyPair;
#[test]
fn paseto_public_verify() {
let sys_rand = SystemRandom::new();
let key_pkcs8 =
Ed25519KeyPair::generate_pkcs8(&sys_rand).expect("Failed to generate pkcs8 key!");
let as_key =
Ed25519KeyPair::from_pkcs8(key_pkcs8.as_ref()).expect("Failed to parse keypair");
// Test messages without footers.
let public_token_one = public_paseto("msg", None, &as_key)
.expect("Failed to public encode msg with no footer!");
// NOTE: This test is just ensuring we can encode a json object, remember these internal impls
// don't check for expires being valid!
let public_token_two = public_paseto(
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}",
None,
&as_key,
)
.expect("Failed to public encode json blob with no footer!");
assert!(public_token_one.starts_with("v2.public."));
assert!(public_token_two.starts_with("v2.public."));
let verified_one = verify_paseto(
&public_token_one.clone(),
None,
as_key.public_key().as_ref(),
);
let verified_two = verify_paseto(&public_token_two, None, as_key.public_key().as_ref());
// Verify the above tokens.
assert!(verified_one.is_ok());
assert!(verified_two.is_ok());
assert_eq!(verified_one.unwrap(), "msg");
assert_eq!(
verified_two.unwrap(),
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}"
);
let should_not_verify_one = verify_paseto(
&public_token_one,
Some("data"),
as_key.public_key().as_ref(),
);
// Verify if it doesn't have a footer in public that it won't pass a verification with a footer.
assert!(should_not_verify_one.is_err());
// Now lets verify with footers.
let public_token_three = public_paseto("msg", Some("footer"), &as_key)
.expect("Failed to public encode msg with footer!");
let public_token_four = public_paseto(
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}",
Some("footer"),
&as_key,
)
.expect("Failed to public encode json blob with footer!");
assert!(public_token_three.starts_with("v2.public."));
assert!(public_token_four.starts_with("v2.public."));
let verified_three = verify_paseto(
&public_token_three,
Some("footer"),
as_key.public_key().as_ref(),
);
let verified_four = verify_paseto(
&public_token_four,
Some("footer"),
as_key.public_key().as_ref(),
);
// Verify the footer tokens.
assert!(verified_three.is_ok());
assert!(verified_four.is_ok());
assert_eq!(verified_three.unwrap(), "msg");
assert_eq!(
verified_four.unwrap(),
"{\"data\": \"yo bro\", \"expires\": \"2018-01-01T00:00:00+00:00\"}"
);
// Validate no footer + invalid footer both fail on tokens encode with footer.
let should_not_verify_two =
verify_paseto(&public_token_three, None, as_key.public_key().as_ref());
let should_not_verify_three = verify_paseto(
&public_token_three,
Some("bleh"),
as_key.public_key().as_ref(),
);
assert!(should_not_verify_two.is_err());
assert!(should_not_verify_three.is_err());
}
}
gitextract_y1ntokrc/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── ISSUE_TEMPLATE.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── dependabot.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE
├── README.md
├── examples/
│ ├── direct-protocol.rs
│ ├── local-using-builders.rs
│ └── public-using-builder.rs
├── rustfmt.toml
└── src/
├── errors.rs
├── lib.rs
├── pae.rs
├── tokens/
│ ├── builder.rs
│ └── mod.rs
├── v1/
│ ├── local.rs
│ ├── mod.rs
│ ├── public.rs
│ ├── signature_rsa_example_private_key.der
│ └── signature_rsa_example_public_key.der
└── v2/
├── local.rs
├── mod.rs
└── public.rs
SYMBOL INDEX (85 symbols across 11 files)
FILE: examples/direct-protocol.rs
function main (line 1) | fn main() {
FILE: examples/local-using-builders.rs
function main (line 8) | fn main() {
function chrono_example (line 20) | fn chrono_example() {
function time_example (line 52) | fn time_example() {
function chrono_and_time_example (line 89) | fn chrono_and_time_example() {
FILE: examples/public-using-builder.rs
function main (line 16) | fn main() {
function chrono_example (line 42) | fn chrono_example() {
function time_example (line 84) | fn time_example() {
function chrono_and_time_example (line 131) | fn chrono_and_time_example() {
FILE: src/errors.rs
type PasetoError (line 9) | pub enum PasetoError {
type SodiumErrors (line 32) | pub enum SodiumErrors {
type RsaKeyErrors (line 61) | pub enum RsaKeyErrors {
type GenericError (line 72) | pub enum GenericError {
FILE: src/pae.rs
function le64 (line 11) | fn le64(mut to_encode: u64) -> Vec<u8> {
function pae (line 25) | pub fn pae(pieces: &[&[u8]]) -> Vec<u8> {
function test_le64 (line 41) | fn test_le64() {
function test_pae (line 47) | fn test_pae() {
FILE: src/tokens/builder.rs
type PasetoBuilder (line 28) | pub struct PasetoBuilder<'a> {
function new (line 46) | pub fn new() -> PasetoBuilder<'a> {
function build (line 64) | pub fn build(&self) -> Result<String, PasetoError> {
method default (line 103) | fn default() -> Self {
function set_rsa_key (line 113) | pub fn set_rsa_key(&'a mut self, private_key_der: &'a [u8]) -> &'a mut S...
function set_ed25519_key (line 124) | pub fn set_ed25519_key(&'a mut self, key_pair: &'a Ed25519KeyPair) -> &'...
function set_encryption_key (line 137) | pub fn set_encryption_key(&'a mut self, encryption_key: &'a [u8]) -> &'a...
function set_footer (line 143) | pub fn set_footer(&'a mut self, footer: &'a str) -> &'a mut Self {
function extend_claims (line 151) | pub fn extend_claims(&'a mut self, claims: HashMap<&'a str, Value>) -> &...
function set_claim (line 157) | pub fn set_claim(&'a mut self, key: &'a str, value: Value) -> &'a mut Se...
function set_audience (line 163) | pub fn set_audience(&'a mut self, audience: &str) -> &'a mut Self {
function set_expiration (line 169) | pub fn set_expiration(&'a mut self, expiration: &DateTime<Utc>) -> &'a m...
function set_expiration (line 175) | pub fn set_expiration(&'a mut self, expiration: &OffsetDateTime) -> &'a ...
function set_expiration_chrono (line 181) | pub fn set_expiration_chrono(&'a mut self, expiration: &DateTime<Utc>) -...
function set_expiration_time (line 187) | pub fn set_expiration_time(&'a mut self, expiration: &OffsetDateTime) ->...
function set_issued_at (line 195) | pub fn set_issued_at(&'a mut self, issued_at: Option<DateTime<Utc>>) -> ...
function set_issued_at (line 203) | pub fn set_issued_at(&'a mut self, issued_at: Option<OffsetDateTime>) ->...
function set_issued_at_chrono (line 214) | pub fn set_issued_at_chrono(&'a mut self, issued_at: Option<DateTime<Utc...
function set_issued_at_time (line 222) | pub fn set_issued_at_time(&'a mut self, issued_at: Option<OffsetDateTime...
function set_issuer (line 230) | pub fn set_issuer(&'a mut self, issuer: &str) -> &'a mut Self {
function set_jti (line 235) | pub fn set_jti(&'a mut self, id: &str) -> &'a mut Self {
function set_not_before (line 241) | pub fn set_not_before(&'a mut self, not_before: &DateTime<Utc>) -> &'a m...
function set_not_before (line 247) | pub fn set_not_before(&'a mut self, not_before: &OffsetDateTime) -> &'a ...
function set_not_before_chrono (line 253) | pub fn set_not_before_chrono(&'a mut self, not_before: &DateTime<Utc>) -...
function set_not_before_time (line 259) | pub fn set_not_before_time(&'a mut self, not_before: &OffsetDateTime) ->...
function set_subject (line 264) | pub fn set_subject(&'a mut self, subject: &str) -> &'a mut Self {
function can_construct_a_token_chrono_with_extend_claims (line 282) | fn can_construct_a_token_chrono_with_extend_claims() {
function can_construct_a_token_chrono (line 329) | fn can_construct_a_token_chrono() {
function can_construct_a_token_time (line 369) | fn can_construct_a_token_time() {
FILE: src/tokens/mod.rs
type PasetoPublicKey (line 34) | pub enum PasetoPublicKey<'a> {
type TimeBackend (line 53) | pub enum TimeBackend {
function validate_potential_json_blob (line 77) | pub fn validate_potential_json_blob(
function validate_local_token (line 205) | pub fn validate_local_token(
function validate_public_token (line 250) | pub fn validate_public_token(
function valid_enc_token_passes_test (line 302) | fn valid_enc_token_passes_test() {
function valid_enc_token_expired_test (line 333) | fn valid_enc_token_expired_test() {
function valid_enc_token_not_before_test (line 368) | fn valid_enc_token_not_before_test() {
function invalid_enc_token_doesnt_validate (line 403) | fn invalid_enc_token_doesnt_validate() {
function valid_pub_token_passes_test (line 438) | fn valid_pub_token_passes_test() {
function validate_pub_key_only_v2 (line 479) | fn validate_pub_key_only_v2() {
function invalid_pub_token_doesnt_validate (line 520) | fn invalid_pub_token_doesnt_validate() {
function allows_validation_without_iat_exp_nbf (line 561) | fn allows_validation_without_iat_exp_nbf() {
FILE: src/v1/local.rs
constant HEADER (line 15) | const HEADER: &str = "v1.local.";
function local_paseto (line 27) | pub fn local_paseto(msg: &str, footer: Option<&str>, key: &[u8]) -> Resu...
function underlying_local_paseto (line 47) | fn underlying_local_paseto(
function calculate_hashed_nonce (line 119) | fn calculate_hashed_nonce(msg: &[u8], random_nonce: &[u8]) -> Vec<u8> {
type CustomKeyWrapper (line 130) | struct CustomKeyWrapper<T>(pub T);
function len (line 133) | fn len(&self) -> usize {
function from (line 139) | fn from(okm: ring::hkdf::Okm<CustomKeyWrapper<usize>>) -> Self {
function decrypt_paseto (line 156) | pub fn decrypt_paseto(
function test_v1_local (line 238) | fn test_v1_local() {
function test_nonce_derivation (line 305) | fn test_nonce_derivation() {
FILE: src/v1/public.rs
constant HEADER (line 13) | const HEADER: &str = "v1.public.";
function public_paseto (line 24) | pub fn public_paseto(
function verify_paseto (line 75) | pub fn verify_paseto(
function test_v1_public (line 127) | fn test_v1_public() {
FILE: src/v2/local.rs
constant HEADER (line 22) | const HEADER: &str = "v2.local.";
function local_paseto (line 34) | pub fn local_paseto(msg: &str, footer: Option<&str>, key: &[u8]) -> Resu...
function underlying_local_paseto (line 54) | fn underlying_local_paseto(
function decrypt_paseto (line 112) | pub fn decrypt_paseto(
function paseto_empty_encrypt_verify (line 168) | fn paseto_empty_encrypt_verify() {
function paseto_non_empty_footer_encrypt_verify (line 199) | fn paseto_non_empty_footer_encrypt_verify() {
function paseto_non_empty_msg_encrypt_verify (line 231) | fn paseto_non_empty_msg_encrypt_verify() {
function full_round_paseto (line 274) | fn full_round_paseto() {
FILE: src/v2/public.rs
constant HEADER (line 12) | const HEADER: &str = "v2.public.";
function public_paseto (line 21) | pub fn public_paseto(
function verify_paseto (line 58) | pub fn verify_paseto(
function paseto_public_verify (line 111) | fn paseto_public_verify() {
Condensed preview — 28 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (108K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 639,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 616,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[FR] New\"\nlabels: enhancement\nassignees: ''\n\n-"
},
{
"path": ".github/ISSUE_TEMPLATE.md",
"chars": 711,
"preview": "Please read the following carefully before opening a new issue.\nYour issue may be closed if it does not provide the info"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 933,
"preview": "<!--\n\nThanks for submitting a PR! We want to make contributing to Paseto as easy as possible.\nPlease read these instruct"
},
{
"path": ".github/dependabot.yml",
"chars": 127,
"preview": "version: 2\nupdates:\n- package-ecosystem: cargo\n directory: \"/\"\n schedule:\n interval: daily\n open-pull-requests-lim"
},
{
"path": ".gitignore",
"chars": 31,
"preview": "/target/\n**/*.rs.bk\nCargo.lock\n"
},
{
"path": "CHANGELOG.md",
"chars": 2372,
"preview": "## <Unreleased>\n\n* documentation overhaul\n* move to thiserror over failure\n* allow setting multiple claims at once in th"
},
{
"path": "CONTRIBUTING.md",
"chars": 782,
"preview": "# Contributing to Paseto #\n\nWe want to make contributing to this project as easy and transparent as\npossible.\n\n## Pull R"
},
{
"path": "Cargo.toml",
"chars": 939,
"preview": "[package]\nname = \"paseto\"\ndescription = \"An alternative token format to JWT\"\nversion = \"3.0.0+1.0.3\"\nrepository = \"https"
},
{
"path": "LICENSE",
"chars": 1050,
"preview": "Copyright 2018 Instructure\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this softwar"
},
{
"path": "README.md",
"chars": 1428,
"preview": "# Paseto Rust #\n\nPaseto is everything you love about JOSE (JWT, JWE, JWS) without any of the [many design deficits that "
},
{
"path": "examples/direct-protocol.rs",
"chars": 1735,
"preview": "fn main() {\n\tlet key = \"YELLOW SUBMARINE, BLACK WIZARDRY\".as_bytes();\n\t#[cfg(feature = \"v2\")]\n\tlet mut key_mut = Vec::fr"
},
{
"path": "examples/local-using-builders.rs",
"chars": 4860,
"preview": "#[cfg(feature = \"easy_tokens_chrono\")]\nuse chrono::prelude::*;\n#[cfg(any(feature = \"easy_tokens_chrono\", feature = \"easy"
},
{
"path": "examples/public-using-builder.rs",
"chars": 6028,
"preview": "#[cfg(all(feature = \"v2\", feature = \"easy_tokens_chrono\"))]\nuse chrono::prelude::*;\n#[cfg(all(\n\tfeature = \"v2\",\n\tany(fea"
},
{
"path": "rustfmt.toml",
"chars": 72,
"preview": "edition = \"2018\"\nhard_tabs = true\nmax_width = 100\nnewline_style = \"Unix\""
},
{
"path": "src/errors.rs",
"chars": 4297,
"preview": "//! A package that contains all of the error types this crate is capable of\n//! producing.\n\nuse thiserror::Error;\n\n/// T"
},
{
"path": "src/lib.rs",
"chars": 7192,
"preview": "//! Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of\n//! the many design deficits that plague the"
},
{
"path": "src/pae.rs",
"chars": 1899,
"preview": "//! Implements \"Pre-Authentication Encoding\". An encoding scheme unique to\n//! paseto. Generally not useful outside of t"
},
{
"path": "src/tokens/builder.rs",
"chars": 12657,
"preview": "//! The token \"builder\" which makes it easy to create JSON encoded Paseto\n//! tokens.\n\n#[cfg(feature = \"v1\")]\nuse crate:"
},
{
"path": "src/tokens/mod.rs",
"chars": 16289,
"preview": "//! \"Easy Tokens\", or helper functions to make building/validating tokens easier.\n//!\n//! The token builder is in genera"
},
{
"path": "src/v1/local.rs",
"chars": 10800,
"preview": "//! [\"Direct\" use of local (symmetrically-encrypted) tokens for V1 of Paseto.](https://github.com/paseto-standard/paseto"
},
{
"path": "src/v1/mod.rs",
"chars": 705,
"preview": "//! [\"Direct\" use of Protocol for V1 of Paseto.](https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b0586"
},
{
"path": "src/v1/public.rs",
"chars": 6449,
"preview": "//! [\"Direct\" use of public (signed but readable) tokens for V1 of Paseto.](https://github.com/paseto-standard/paseto-sp"
},
{
"path": "src/v2/local.rs",
"chars": 8379,
"preview": "//! [\"Direct\" use of local (symmetrically-encrypted) tokens for V2 of Paseto.](https://github.com/paseto-standard/paseto"
},
{
"path": "src/v2/mod.rs",
"chars": 705,
"preview": "//! [\"Direct\" use of Protocol for V2 of Paseto.](https://github.com/paseto-standard/paseto-spec/blob/8b3fed8240e203b0586"
},
{
"path": "src/v2/public.rs",
"chars": 6487,
"preview": "//! [\"Direct\" use of public (signed but readable) tokens for V2 of Paseto.](https://github.com/paseto-standard/paseto-sp"
}
]
// ... and 2 more files (download for full content)
About this extraction
This page contains the full source code of the instructure/paseto GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 28 files (95.9 KB), approximately 28.8k tokens, and a symbol index with 85 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.