Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,28 @@ categories = [
]
documentation = "https://docs.rs/rusty_paseto/latest/rusty_paseto/"

[package.metadata.docs.rs]
# ARCHITECTURAL DESIGN: This crate uses mutually exclusive features by design.
# The PASETO specification recommends choosing a single version per application.
# Multiple public features cannot coexist due to intentionally conflicting trait
# implementations - this enforces compile-time safety and prevents version mixing.
# The feature-gated architecture minimizes binary size by only compiling the
# cryptographic dependencies you actually use.
all-features = false
# Build docs with default features (batteries_included + v4_local + v4_public)
# This showcases the complete API with the modern recommended PASETO version
features = ["batteries_included", "v4_local", "v4_public"]

[features]
v1 = []
v2 = []
v3 = []
v4 = []
public = []
local = []
v1_local = ["v1", "local", "core", "aes", "chacha20", "hmac", "sha2", "blake2"]
v1_local = ["v1", "local", "core", "aes", "ctr", "chacha20", "hmac", "sha2", "blake2"]
v2_local = ["v2", "local", "core", "blake2", "chacha20poly1305"]
v3_local = ["v3", "local", "core", "aes", "hmac", "sha2", "chacha20"]
v3_local = ["v3", "local", "core", "aes", "ctr", "hmac", "sha2", "chacha20"]
v4_local = ["v4", "local", "core", "blake2", "chacha20"]
v1_public = ["v1", "public", "core", "ed25519-dalek"]
v2_public = ["v2", "public", "core", "ed25519-dalek", "ring/std"]
Expand All @@ -52,21 +64,23 @@ hex = { version = "0.4", optional = false }
serde = { version = "1.0", features = ["derive"], optional = true }
ed25519-dalek = { version = "2.0", features = ["zeroize"], optional = true }
serde_json = { version = "1.0", optional = true }
thiserror = "1.0"
thiserror = "2.0"
iso8601 = "0.6"
erased-serde = { version = "0.4", optional = true }
aes = { version = "0.7", features = ["ctr"], optional = true }
aes = { version = "0.8", optional = true }
ctr = { version = "0.9", optional = true }
hmac = { version = "0.12", optional = true }
sha2 = { version = "0.10", optional = true }
subtle = "2.6"
zeroize = { version = "1.4", features = ["zeroize_derive"] }
time = { version = "0.3", features = ["parsing", "formatting"] }
rand_core = "0.6"
rand_core = "0.9"
digest = "0.10"

[dev-dependencies]
anyhow = "1.0"
serde_json = { version = "1.0" }
primes = "0.3"
primes = "0.4"
actix-web = "4"
actix-identity = "0.4"
tokio = "1.17"
Expand Down
9 changes: 6 additions & 3 deletions src/core/common/cipher_text_impl/v1_local.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
#![cfg(feature = "v1_local")]
use std::marker::PhantomData;
use aes::Aes256Ctr;
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{NewCipher, StreamCipher};
use aes::Aes256;
use ctr::cipher::{KeyIvInit, StreamCipher};
use ctr::cipher::generic_array::GenericArray;
use ctr::Ctr128BE;
use crate::core::common::cipher_text::CipherText;
use crate::core::{Local, V1};
use crate::core::common::EncryptionKey;

type Aes256Ctr = Ctr128BE<Aes256>;

impl CipherText<V1, Local> {
pub(crate) fn from(payload: &[u8], encryption_key: &EncryptionKey<V1, Local>) -> Self {
let key = GenericArray::from_slice(encryption_key.as_ref());
Expand Down
9 changes: 6 additions & 3 deletions src/core/common/cipher_text_impl/v3_local.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#![cfg(feature = "v3_local")]
use std::marker::PhantomData;
use aes::Aes256Ctr;
use aes::cipher::generic_array::GenericArray;
use aes::cipher::{NewCipher, StreamCipher};
use aes::Aes256;
use ctr::cipher::{KeyIvInit, StreamCipher};
use ctr::cipher::generic_array::GenericArray;
use ctr::Ctr128BE;
use crate::core::common::{CipherText, EncryptionKey};
use crate::core::{Local, V3};

type Aes256Ctr = Ctr128BE<Aes256>;

impl CipherText<V3, Local> {
pub(crate) fn from(payload: &[u8], encryption_key: &EncryptionKey<V3, Local>) -> Self {
let key = GenericArray::from_slice(encryption_key.as_ref());
Expand Down
6 changes: 3 additions & 3 deletions src/core/common/pre_authentication_encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ pub struct PreAuthenticationEncoding(Vec<u8>);
///
impl PreAuthenticationEncoding {
/// * `pieces` - The Pieces to concatenate, and encode together.
/// Refactored from original code found at
/// <https://github.com/instructure/paseto/blob/trunk/src/pae.rs>
/// Refactored from original code found at
/// <https://github.com/instructure/paseto/blob/trunk/src/pae.rs>
pub fn parse<'a>(pieces: &'a [&'a [u8]]) -> Self {
let the_vec = PreAuthenticationEncoding::le64(pieces.len() as u64);

Expand All @@ -21,7 +21,7 @@ impl PreAuthenticationEncoding {
/// Encodes a u64-bit unsigned integer into a little-endian binary string.
///
/// * `to_encode` - The u8 to encode.
/// Copied and gently refactored from <https://github.com/instructure/paseto/blob/trunk/src/pae.rs>
/// Copied and gently refactored from <https://github.com/instructure/paseto/blob/trunk/src/pae.rs>
pub(crate) fn le64(mut to_encode: u64) -> Vec<u8> {
let mut the_vec = Vec::with_capacity(8);

Expand Down
2 changes: 0 additions & 2 deletions src/core/key/paseto_nonce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ use std::ops::Deref;
/// # }
/// # Ok::<(),anyhow::Error>(())
/// ```


pub struct PasetoNonce<'a, Version, Purpose> {
pub(crate) version: PhantomData<Version>,
pub(crate) purpose: PhantomData<Purpose>,
Expand Down
3 changes: 1 addition & 2 deletions src/core/paseto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use crate::core::{Base64Encodable, Footer, Header, ImplicitAssertion, ImplicitAs


/// Used to build and encrypt / decrypt core PASETO tokens

///
/// Given a [Payload], optional [Footer] and optional [ImplicitAssertion] ([V3] or [V4] only)
/// returns an encrypted token when [Local] is specified as the purpose or a signed token when
Expand Down Expand Up @@ -144,7 +143,7 @@ impl<'a, Version: VersionTrait, Purpose: PurposeTrait> Paseto<'a, Version, Purpo

pub(crate) fn parse_raw_token(
raw_token: &'a str,
footer: (impl Into<Option<Footer<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
v: &Version,
p: &Purpose,
) -> Result<Vec<u8>, PasetoError> {
Expand Down
8 changes: 5 additions & 3 deletions src/core/paseto_impl/v1_local.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#![cfg(feature = "v1_local")]
use std::str;
use hmac::{Hmac, Mac};
use subtle::ConstantTimeEq;
use crate::core::{Footer, Header, Key, Local, Paseto, PasetoError, PasetoNonce, PasetoSymmetricKey, V1};
use crate::core::common::{AuthenticationKey, AuthenticationKeySeparator, CipherText, EncryptionKey, EncryptionKeySeparator, PreAuthenticationEncoding, RawPayload, Tag};
use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals;
use sha2::Sha384;

impl<'a> Paseto<'a, V1, Local> {
Expand All @@ -26,7 +26,7 @@ impl<'a> Paseto<'a, V1, Local> {
pub fn try_decrypt(
token: &'a str,
key: &PasetoSymmetricKey<V1, Local>,
footer: (impl Into<Option<Footer<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
) -> Result<String, PasetoError> {
let decoded_payload = Self::parse_raw_token(token, footer, &V1::default(), &Local::default())?;
let nonce = Key::from(&decoded_payload[..32]);
Expand All @@ -51,7 +51,9 @@ impl<'a> Paseto<'a, V1, Local> {
let tag = &decoded_payload[(nonce.len() + ciphertext.len())..];
let tag2 = &Tag::<V1, Local>::from(authentication_key, &pae);
//compare tags
ConstantTimeEquals(tag, tag2)?;
if !bool::from(tag.ct_eq(tag2.as_ref())) {
return Err(PasetoError::Cryption);
}

//decrypt payload
let ciphertext = CipherText::<V1, Local>::from(ciphertext, &encryption_key);
Expand Down
2 changes: 1 addition & 1 deletion src/core/paseto_impl/v1_public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ impl<'a> Paseto<'a, V1, Public> {
pub fn try_verify(
signature: &'a str,
public_key: &PasetoAsymmetricPublicKey<V1, Public>,
footer: (impl Into<Option<Footer<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
) -> Result<String, PasetoError> {
let decoded_payload = Self::parse_raw_token(signature, footer, &V1::default(), &Public::default())?;

Expand Down
2 changes: 1 addition & 1 deletion src/core/paseto_impl/v2_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl<'a> Paseto<'a, V2, Local> {
pub fn try_decrypt(
token: &'a str,
key: &PasetoSymmetricKey<V2, Local>,
footer: (impl Into<Option<Footer<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
) -> Result<String, PasetoError> {
//get footer

Expand Down
2 changes: 1 addition & 1 deletion src/core/paseto_impl/v2_public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ impl<'a> Paseto<'a, V2, Public> {
pub fn try_verify(
signature: &'a str,
public_key: &PasetoAsymmetricPublicKey<V2, Public>,
footer: (impl Into<Option<Footer<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
) -> Result<String, PasetoError> {
let decoded_payload = Self::parse_raw_token(signature, footer, &V2::default(), &Public::default())?;

Expand Down
10 changes: 6 additions & 4 deletions src/core/paseto_impl/v3_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::str;

use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals;
use subtle::ConstantTimeEq;

use crate::core::{Footer, Header, ImplicitAssertion, Key, Local, Paseto, PasetoError, PasetoNonce, PasetoSymmetricKey, V3};
use crate::core::common::{AuthenticationKey, AuthenticationKeySeparator, CipherText, EncryptionKey, EncryptionKeySeparator, PreAuthenticationEncoding, RawPayload, Tag};
Expand All @@ -27,8 +27,8 @@ impl<'a> Paseto<'a, V3, Local> {
pub fn try_decrypt(
token: &'a str,
key: &PasetoSymmetricKey<V3, Local>,
footer: (impl Into<Option<Footer<'a>>> + Copy),
implicit_assertion: (impl Into<Option<ImplicitAssertion<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
implicit_assertion: impl Into<Option<ImplicitAssertion<'a>>> + Copy,
) -> Result<String, PasetoError> {
//get footer

Expand All @@ -55,7 +55,9 @@ impl<'a> Paseto<'a, V3, Local> {
let tag = &decoded_payload[(nonce.len() + ciphertext.len())..];
let tag2 = &Tag::<V3, Local>::from(authentication_key, &pae);
//compare tags
ConstantTimeEquals(tag, tag2)?;
if !bool::from(tag.ct_eq(tag2.as_ref())) {
return Err(PasetoError::Cryption);
}

//decrypt payload
let ciphertext = CipherText::<V3, Local>::from(ciphertext, &encryption_key);
Expand Down
4 changes: 2 additions & 2 deletions src/core/paseto_impl/v3_public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ impl<'a> Paseto<'a, V3, Public> {
pub fn try_verify(
signature: &'a str,
public_key: &PasetoAsymmetricPublicKey<V3, Public>,
footer: (impl Into<Option<Footer<'a>>> + Copy),
implicit_assertion: (impl Into<Option<ImplicitAssertion<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
implicit_assertion: impl Into<Option<ImplicitAssertion<'a>>> + Copy,
) -> Result<String, PasetoError> {
let decoded_payload = Self::parse_raw_token(signature, footer, &V3::default(), &Public::default())?;

Expand Down
10 changes: 6 additions & 4 deletions src/core/paseto_impl/v4_local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use std::str;

use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals;
use subtle::ConstantTimeEq;

use crate::core::{Footer, Header, ImplicitAssertion, Key, Local, Paseto, PasetoError, PasetoNonce, PasetoSymmetricKey, V4};
use crate::core::common::{AuthenticationKey, AuthenticationKeySeparator, CipherText, EncryptionKey, EncryptionKeySeparator, PreAuthenticationEncoding, RawPayload, Tag};
Expand All @@ -27,8 +27,8 @@ impl<'a> Paseto<'a, V4, Local> {
pub fn try_decrypt(
token: &'a str,
key: &PasetoSymmetricKey<V4, Local>,
footer: (impl Into<Option<Footer<'a>>> + Copy),
implicit_assertion: (impl Into<Option<ImplicitAssertion<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
implicit_assertion: impl Into<Option<ImplicitAssertion<'a>>> + Copy,
) -> Result<String, PasetoError> {
//get footer

Expand All @@ -55,7 +55,9 @@ impl<'a> Paseto<'a, V4, Local> {
let tag = &decoded_payload[(nonce.len() + ciphertext.len())..];
let tag2 = &Tag::<V4, Local>::from(authentication_key, &pae);
//compare tags
ConstantTimeEquals(tag, tag2)?;
if !bool::from(tag.ct_eq(tag2.as_ref())) {
return Err(PasetoError::Cryption);
}

//decrypt payload
let ciphertext = CipherText::<V4, Local>::from(ciphertext, &encryption_key);
Expand Down
4 changes: 2 additions & 2 deletions src/core/paseto_impl/v4_public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ impl<'a> Paseto<'a, V4, Public> {
pub fn try_verify(
signature: &'a str,
public_key: &PasetoAsymmetricPublicKey<V4, Public>,
footer: (impl Into<Option<Footer<'a>>> + Copy),
implicit_assertion: (impl Into<Option<ImplicitAssertion<'a>>> + Copy),
footer: impl Into<Option<Footer<'a>>> + Copy,
implicit_assertion: impl Into<Option<ImplicitAssertion<'a>>> + Copy,
) -> Result<String, PasetoError> {
let decoded_payload = Self::parse_raw_token(signature, footer, &V4::default(), &Public::default())?;

Expand Down
4 changes: 2 additions & 2 deletions src/core/traits.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use base64::DecodeError;
use base64::prelude::*;
use ring::constant_time::verify_slices_are_equal as ConstantTimeEquals;
use subtle::ConstantTimeEq;
use std::fmt::Display;

//marker traits
Expand All @@ -27,6 +27,6 @@ pub(crate) trait Base64Encodable<T: ?Sized + AsRef<[u8]>>: Display + AsRef<T> {
where
B: AsRef<str>,
{
ConstantTimeEquals(self.encode().as_ref(), other.as_ref().as_bytes()).is_ok()
self.encode().as_bytes().ct_eq(other.as_ref().as_bytes()).into()
}
}
1 change: 0 additions & 1 deletion src/generic/builders/generic_builder.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use core::marker::PhantomData;
use std::collections::HashMap;

use erased_serde::Serialize;
use serde_json::{Map, Value};

use crate::generic::*;
Expand Down
1 change: 0 additions & 1 deletion src/generic/claims/custom_claim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ use serde::ser::SerializeMap;
/// # assert_eq!(json_value["Universe"], 136);
/// # }
/// # Ok::<(),anyhow::Error>(())
/// ```

#[derive(Clone, Debug)]
pub struct CustomClaim<T>((String, T));
Expand Down
32 changes: 29 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//! This crate is a type-driven, ergonomic implementation of the [PASETO](https://github.com/paseto-standard/paseto-spec) protocol for secure stateless tokens.
//!
//! > "Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of the
//! [many design deficits that plague the JOSE standards](https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid)."
//!> [many design deficits that plague the JOSE standards](https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid)."
//! > -- [PASETO Specification](https://github.com/paseto-standard/paseto-spec)
//!
//!
Expand Down Expand Up @@ -151,7 +151,7 @@
//!
//! * Has no [footer](https://github.com/paseto-standard/paseto-spec/tree/master/docs)
//! * Has no [implicit assertion](https://github.com/paseto-standard/paseto-spec/tree/master/docs)
//! for V3 or V4 versioned tokens
//! for V3 or V4 versioned tokens
//! * Expires in **1 hour** after creation (due to an included default ExpirationClaim)
//! * Contains an IssuedAtClaim defaulting to the current utc time the token was created
//! * Contains a NotBeforeClaim defaulting to the current utc time the token was created
Expand Down Expand Up @@ -181,7 +181,7 @@
//!
//! * Validates the token structure and decryptes the payload or verifies the signature of the content
//! * Validates the [footer](https://github.com/paseto-standard/paseto-spec/tree/master/docs) if
//! one was provided
//! one was provided
//! * Validates the [implicit assertion](https://github.com/paseto-standard/paseto-spec/tree/master/docs) if one was provided (for V3 or V4 versioned tokens only)
//!
//! ## A token with a footer
Expand Down Expand Up @@ -535,6 +535,32 @@
//!
//!

// Compile-time checks for incompatible feature combinations
// Multiple public features cause conflicting trait implementations for PasetoError
#[cfg(all(feature = "v3_public", any(feature = "v1_public", feature = "v2_public", feature = "v4_public")))]
compile_error!(
"Cannot enable v3_public with other public features due to conflicting trait implementations. \n\
Choose only ONE public feature: v1_public, v2_public, v3_public, or v4_public. \n\
The PASETO specification recommends using a single version throughout your application. \n\
See: https://github.com/rrrodzilla/rusty_paseto/issues/48"
);

#[cfg(all(feature = "v1_public", any(feature = "v2_public", feature = "v4_public")))]
compile_error!(
"Cannot enable multiple public features (v1_public with v2_public or v4_public) due to conflicting trait implementations. \n\
Choose only ONE public feature: v1_public, v2_public, v3_public, or v4_public. \n\
The PASETO specification recommends using a single version throughout your application. \n\
See: https://github.com/rrrodzilla/rusty_paseto/issues/48"
);

#[cfg(all(feature = "v2_public", feature = "v4_public"))]
compile_error!(
"Cannot enable both v2_public and v4_public due to conflicting trait implementations. \n\
Choose only ONE public feature: v1_public, v2_public, v3_public, or v4_public. \n\
The PASETO specification recommends using a single version throughout your application. \n\
See: https://github.com/rrrodzilla/rusty_paseto/issues/48"
);

//public interface
#[cfg(feature = "core")]
pub mod core;
Expand Down
1 change: 0 additions & 1 deletion tests/build_payload_from_claims_prop_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
use std::collections::HashMap;

use proptest::prelude::*;
use erased_serde::Serialize;
use serde_json::{Map, Number, Value};

// Define a strategy to generate arbitrary JSON values
Expand Down
Loading
Loading