chore: initial commit

This commit is contained in:
itsscb 2025-01-07 01:12:06 +01:00
parent b1a0c97cab
commit 0bf2b5f63b
14 changed files with 970 additions and 2 deletions

7
.gitignore vendored
View File

@ -18,4 +18,9 @@ Cargo.lock
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
#.idea/
# Added by cargo
/target
.vscode/

20
Cargo.toml Normal file
View File

@ -0,0 +1,20 @@
[package]
name = "paseto_maker"
authors = ["itsscb <dev@itsscb.de>"]
license = "GPL-3.0"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/itsscb/paseto_maker"
description = "This library provides high-level functionality for creating, handling, and managing PASETO tokens."
[dependencies]
chrono = { version = "0.4.39", features = ["serde"] }
ed25519-dalek = { version = "2.1.1", features = ["rand_core"] }
rand = "0.8.5"
rusty_paseto = { version = "0.7.2", features = [
"batteries_included",
"v4_public",
] }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134"
thiserror = "2.0.9"

View File

@ -1,2 +1,46 @@
# paseto_maker
Rust lib to create and verify paseto tokens and add and extract claims
This library provides high-level functionality for creating, handling, and managing PASETO tokens.
**Note:** This crate is currently in Alpha. The API is subject to change and may contain bugs.
# Overview
This library includes modules for defining claims, handling errors, and creating/verifying PASETO tokens.
It leverages the `rusty_paseto` crate and currently supports PASETO Tokens V4.public.
# Modules
- `claims`: Defines the structure and behavior of the claims that can be embedded in a PASETO token.
- `errors`: Provides error types and handling mechanisms for the library.
- `maker`: Contains the logic for creating and verifying PASETO tokens.
# Re-exports
- `Claims`: The struct representing the claims in a PASETO token.
- `Maker`: The struct used for creating and verifying PASETO tokens.
# Usage Example
```rust
use paseto_maker::{Maker, Claims, version::V4, purpose::Public};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let maker = Maker::new_with_keypair().unwrap();
let claims = Claims::new().with_subject("example");
let token = maker.create_token(&claims).unwrap();
println!("Token: {}", token);
let verified_claims = maker.verify_token(&token)?;
println!("Verified Claims: {:?}", verified_claims);
Ok(())
}
```
The `claims` module defines the structure and behavior of the claims that can be embedded in a PASETO token.
The `errors` module provides error types and handling mechanisms for the library.
The `maker` module contains the logic for creating and verifying PASETO tokens.
The `Claims` struct and `Maker` struct are re-exported for ease of use.
This library uses the `rusty_paseto` crate underneath and currently only supports PASETO Tokens V4.public.

96
flake.lock generated Normal file
View File

@ -0,0 +1,96 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1736042175,
"narHash": "sha256-jdd5UWtLVrNEW8K6u5sy5upNAFmF3S4Y+OIeToqJ1X8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "bf689c40d035239a489de5997a4da5352434632e",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1728538411,
"narHash": "sha256-f0SBJz1eZ2yOuKUr5CA9BHULGXVSn6miBuUWdTyhUhU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b69de56fac8c2b6f8fd27f2eca01dcda8e0a4221",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1736130662,
"narHash": "sha256-z+WGez9oTR2OsiUWE5ZhIpETqM1ogrv6Xcd24WFi6KQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "2f5d4d9cd31cc02c36e51cb2e21c4b25c4f78c52",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

51
flake.nix Normal file
View File

@ -0,0 +1,51 @@
{
description = "Example Rust development environment for Zero to Nix";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs {
inherit system overlays;
};
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [ "rust-src" "rust-analyzer" "clippy" "rustfmt" ];
targets = [ "x86_64-unknown-linux-gnu" ];
};
in
{
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
rustToolchain
clippy
cargo-edit
cargo-binstall
bacon
openssl
pkg-config
# Add rust-analyzer separately to ensure it's the latest version
# rust-analyzer
];
shellHook = ''
export PATH=${rustToolchain}/bin:$PATH
export RUSTC_VERSION=$(rustc --version)
export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"
export OPENSSL_DIR="${pkgs.openssl.dev}"
export OPENSSL_LIB_DIR="${pkgs.openssl.out}/lib"
export OPENSSL_INCLUDE_DIR="${pkgs.openssl.dev}/include"
# Add this line to explicitly set the rust-analyzer path
export RUST_ANALYZER_PATH="${pkgs.rust-analyzer}/bin/rust-analyzer"
'';
packages = pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs; [ libiconv ]);
};
}
);
}

326
src/claims/mod.rs Normal file
View File

@ -0,0 +1,326 @@
use crate::errors::ClaimError;
use chrono::{DateTime, Utc};
use serde::{de::DeserializeOwned, Serialize};
use serde_json::Value;
use std::{
collections::BTreeMap,
fmt::{self, Display, Formatter},
sync::Arc,
};
pub mod reserved;
/// Represents a collection of claims for a token.
///
/// Claims are stored in a `BTreeMap` with `Arc<str>` keys and `serde_json::Value` values.
///
/// # Examples
///
/// ```
/// use paseto_maker::Claims;
///
/// let claims = Claims::new()
/// .with_subject("1234567890")
/// .with_issuer("issuer")
/// .with_audience("audience")
/// .with_expiration("2023-10-01T00:00:00+00:00")
/// .with_not_before("2023-09-01T00:00:00+00:00")
/// .with_issued_at("2023-09-01T00:00:00+00:00")
/// .with_token_identifier("token_id");
///
/// let subject: Option<String> = claims.get_subject();
/// assert_eq!(subject, Some("1234567890".to_string()));
/// ```
///
/// # Methods
///
/// - `new`: Creates a new, empty `Claims` instance.
/// - `with_subject`: Adds a subject claim.
/// - `with_issuer`: Adds an issuer claim.
/// - `with_audience`: Adds an audience claim.
/// - `with_expiration`: Adds an expiration claim.
/// - `with_not_before`: Adds a not-before claim.
/// - `with_issued_at`: Adds an issued-at claim.
/// - `with_token_identifier`: Adds a token identifier claim.
/// - `set_claim`: Sets a custom claim with a specified key and value.
/// - `get_claim`: Retrieves a claim by key and attempts to deserialize it into the specified type.
/// - `get_subject`: Retrieves the subject claim.
/// - `get_issuer`: Retrieves the issuer claim.
/// - `get_audience`: Retrieves the audience claim.
/// - `get_expiration`: Retrieves the expiration claim.
/// - `get_not_before`: Retrieves the not-before claim.
/// - `get_issued_at`: Retrieves the issued-at claim.
/// - `get_token_identifier`: Retrieves the token identifier claim.
/// - `iter`: Returns an iterator over the claims.
///
/// # Errors
///
/// - `set_claim` will return an error if the value cannot be serialized or is null.
/// # Examples
///
/// ```
/// use paseto_maker::Claims;
///
/// let mut claims = Claims::new();
/// claims.set_claim("sub", "1234567890").unwrap();
/// claims.set_claim("name", "John Doe").unwrap();
/// claims.set_claim("admin", true).unwrap();
///
/// let sub: Option<String> = claims.get_claim("sub");
/// assert_eq!(sub, Some("1234567890".to_string()));
///
/// let name: Option<String> = claims.get_claim("name");
/// assert_eq!(name, Some("John Doe".to_string()));
///
/// let admin: Option<bool> = claims.get_claim("admin");
/// assert_eq!(admin, Some(true));
/// ```
///
/// ```
/// use paseto_maker::Claims;
/// use chrono::{DateTime, Utc};
///
/// let claims = Claims::new()
/// .with_subject("1234567890")
/// .with_issuer("issuer")
/// .with_audience("audience")
/// .with_expiration("2023-10-01T00:00:00+00:00")
/// .with_not_before("2023-09-01T00:00:00+00:00")
/// .with_issued_at("2023-09-01T00:00:00+00:00")
/// .with_token_identifier("token_id");
///
/// let subject: Option<String> = claims.get_subject();
/// assert_eq!(subject, Some("1234567890".to_string()));
///
/// let issuer: Option<String> = claims.get_issuer();
/// assert_eq!(issuer, Some("issuer".to_string()));
///
/// let audience: Option<String> = claims.get_audience();
/// assert_eq!(audience, Some("audience".to_string()));
///
/// let expiration: Option<DateTime<Utc>> = claims.get_expiration();
/// assert_eq!(expiration.unwrap().to_rfc3339().to_string(), "2023-10-01T00:00:00+00:00".to_string());
///
/// let not_before: Option<DateTime<Utc>> = claims.get_not_before();
/// assert_eq!(not_before.unwrap().to_rfc3339().to_string(), "2023-09-01T00:00:00+00:00".to_string());
///
/// let issued_at: Option<DateTime<Utc>> = claims.get_issued_at();
/// assert_eq!(issued_at.unwrap().to_rfc3339().to_string(), "2023-09-01T00:00:00+00:00".to_string());
///
/// let token_identifier: Option<String> = claims.get_token_identifier();
/// assert_eq!(token_identifier, Some("token_id".to_string()));
/// ```
#[derive(Debug, Default)]
pub struct Claims {
claims: BTreeMap<Arc<str>, Value>,
}
impl Display for Claims {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let claims: Vec<String> = self
.claims
.iter()
.map(|(k, v)| format!("{k}: {v}"))
.collect();
write!(f, "{}", claims.join(", "))
}
}
impl From<Value> for Claims {
fn from(value: Value) -> Self {
let claims: BTreeMap<Arc<str>, Value> = match value {
Value::Object(map) => map.into_iter().map(|(k, v)| (Arc::from(k), v)).collect(),
_ => BTreeMap::new(),
};
Self { claims }
}
}
impl Claims {
pub fn iter(&self) -> impl Iterator<Item = (&Arc<str>, &Value)> {
self.claims.iter()
}
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_subject<T: AsRef<str>>(mut self, subject: T) -> Self {
self.claims
.insert(Arc::from(reserved::SUBJECT), subject.as_ref().into());
self
}
#[must_use]
pub fn with_issuer<T: AsRef<str>>(mut self, issuer: T) -> Self {
self.claims
.insert(Arc::from(reserved::ISSUER), issuer.as_ref().into());
self
}
#[must_use]
pub fn with_audience<T: AsRef<str>>(mut self, audience: T) -> Self {
self.claims
.insert(Arc::from(reserved::AUDIENCE), audience.as_ref().into());
self
}
#[must_use]
pub fn with_expiration<T: AsRef<str>>(mut self, expiration: T) -> Self {
self.claims
.insert(Arc::from(reserved::EXPIRATION), expiration.as_ref().into());
self
}
#[must_use]
pub fn with_not_before<T: AsRef<str>>(mut self, not_before: T) -> Self {
self.claims
.insert(Arc::from(reserved::NOT_BEFORE), not_before.as_ref().into());
self
}
#[must_use]
pub fn with_issued_at<T: AsRef<str>>(mut self, issued_at: T) -> Self {
self.claims
.insert(Arc::from(reserved::ISSUED_AT), issued_at.as_ref().into());
self
}
#[must_use]
pub fn with_token_identifier<T: AsRef<str>>(mut self, token_identifier: T) -> Self {
self.claims.insert(
Arc::from(reserved::TOKEN_IDENTIFIER),
token_identifier.as_ref().into(),
);
self
}
/// # Errors
///
/// This function will return an error if the value cannot be serialized.
/// * `get_claim` - Retrieves a claim by key and attempts to deserialize it into the specified type.
///
pub fn set_claim<T: Serialize>(&mut self, key: &str, value: T) -> Result<(), ClaimError> {
let value = serde_json::to_value(value)?;
if value.is_null() {
return Err(ClaimError::InvalidValue);
}
self.claims.insert(Arc::from(key), value);
Ok(())
}
#[must_use]
pub fn get_claim<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
self.claims
.get(&Arc::from(key))
.and_then(|value| serde_json::from_value(value.clone()).ok())
}
#[must_use]
pub fn get_subject(&self) -> Option<String> {
self.get_claim(reserved::SUBJECT)
}
#[must_use]
pub fn get_issuer(&self) -> Option<String> {
self.get_claim(reserved::ISSUER)
}
#[must_use]
pub fn get_audience(&self) -> Option<String> {
self.get_claim(reserved::AUDIENCE)
}
#[must_use]
pub fn get_expiration(&self) -> Option<DateTime<Utc>> {
self.get_claim(reserved::EXPIRATION)
}
#[must_use]
pub fn get_not_before(&self) -> Option<DateTime<Utc>> {
self.get_claim(reserved::NOT_BEFORE)
}
#[must_use]
pub fn get_issued_at(&self) -> Option<DateTime<Utc>> {
self.get_claim(reserved::ISSUED_AT)
}
#[must_use]
pub fn get_token_identifier(&self) -> Option<String> {
self.get_claim(reserved::TOKEN_IDENTIFIER)
}
}
#[cfg(test)]
mod test {
use serde_json::json;
use super::*;
#[test]
fn test_set_and_get_claim() {
let mut claims = Claims::new();
claims.set_claim("sub", "1234567890").unwrap();
claims.set_claim("name", "John Doe").unwrap();
claims.set_claim("admin", true).unwrap();
let sub: Option<String> = claims.get_claim("sub");
assert_eq!(sub, Some("1234567890".to_string()));
let name: Option<String> = claims.get_claim("name");
assert_eq!(name, Some("John Doe".to_string()));
let admin: Option<bool> = claims.get_claim("admin");
assert_eq!(admin, Some(true));
}
#[test]
fn test_builder() {
let claims = Claims::new()
.with_subject("1234567890")
.with_audience("test audience")
.with_issued_at("2019-01-01T00:00:00+00:00");
let sub: Option<String> = claims.get_claim("sub");
assert_eq!(sub, Some("1234567890".to_string()));
let sub: Option<String> = claims.get_claim("aud");
assert_eq!(sub, Some("test audience".to_string()));
let sub: Option<String> = claims.get_claim("iat");
assert_eq!(sub, Some("2019-01-01T00:00:00+00:00".to_string()));
}
#[test]
fn test_iter() {
let mut claims = Claims::new();
claims.set_claim("sub", "1234567890").unwrap();
claims.set_claim("name", "John Doe").unwrap();
claims.set_claim("admin", true).unwrap();
let mut iter = claims.iter();
assert_eq!(iter.next(), Some((&Arc::from("admin"), &json!(true))));
assert_eq!(iter.next(), Some((&Arc::from("name"), &json!("John Doe"))));
assert_eq!(iter.next(), Some((&Arc::from("sub"), &json!("1234567890"))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_get_claim_nonexistent() {
let claims = Claims::new();
let value: Option<String> = claims.get_claim("nonexistent");
assert_eq!(value, None);
}
#[test]
fn test_set_claim_error() {
let mut claims = Claims::new();
let result = claims.set_claim("invalid", f64::NAN);
dbg!(&result);
assert!(result.is_err());
}
}

7
src/claims/reserved.rs Normal file
View File

@ -0,0 +1,7 @@
pub const ISSUER: &str = "iss";
pub const SUBJECT: &str = "sub";
pub const AUDIENCE: &str = "aud";
pub const EXPIRATION: &str = "exp";
pub const NOT_BEFORE: &str = "nbf";
pub const ISSUED_AT: &str = "iat";
pub const TOKEN_IDENTIFIER: &str = "jti";

38
src/errors.rs Normal file
View File

@ -0,0 +1,38 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ClaimError {
#[error("invalid value")]
InvalidValue,
#[error("serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("paseto error: {0}")]
PasetoError(#[from] rusty_paseto::generic::PasetoError),
#[error("invalid claim: {0}")]
InvalidClaim(#[from] rusty_paseto::generic::PasetoClaimError),
}
#[allow(dead_code)]
#[derive(Debug, Error)]
pub enum TokenError {
#[error("Invalid claim: {0}")]
InvalidClaim(String),
#[error("Token expired")]
Expired,
#[error("Token not valid")]
Invalid,
#[error("Token validation failed")]
Validation,
#[error("Token malformed")]
Format,
#[error("Claim error: {0}")]
ClaimError(#[from] ClaimError),
#[error("Token creation failed: {0}")]
TokenCreationFailed(String),
}
#[derive(Error, Debug)]
pub enum MakerError {
#[error("Invalid key: {0}")]
InvalidKey(String),
}

49
src/lib.rs Normal file
View File

@ -0,0 +1,49 @@
//! This library provides high-level functionality for creating, handling, and managing PASETO tokens.
//!
//! **Note:** This crate is currently in Alpha. The API is subject to change and may contain bugs.
//!
//! # Overview
//! This library includes modules for defining claims, handling errors, and creating/verifying PASETO tokens.
//! It leverages the `rusty_paseto` crate and currently supports PASETO Tokens V4.public.
//!
//! # Modules
//! - `claims`: Defines the structure and behavior of the claims that can be embedded in a PASETO token.
//! - `errors`: Provides error types and handling mechanisms for the library.
//! - `maker`: Contains the logic for creating and verifying PASETO tokens.
//!
//! # Re-exports
//! - `Claims`: The struct representing the claims in a PASETO token.
//! - `Maker`: The struct used for creating and verifying PASETO tokens.
//!
//! # Usage Example
//! ```rust
//! use paseto_maker::{Maker, Claims, version::V4, purpose::Public};
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let maker = Maker::new_with_keypair().unwrap();
//! let claims = Claims::new().with_subject("example");
//! let token = maker.create_token(&claims).unwrap();
//! println!("Token: {}", token);
//!
//! let verified_claims = maker.verify_token(&token)?;
//! println!("Verified Claims: {:?}", verified_claims);
//! Ok(())
//! }
//! ```
//!
//! The `claims` module defines the structure and behavior of the claims that can be embedded in a PASETO token.
//! The `errors` module provides error types and handling mechanisms for the library.
//! The `maker` module contains the logic for creating and verifying PASETO tokens.
//!
//! The `Claims` struct and `Maker` struct are re-exported for ease of use.
//!
//! This library uses the `rusty_paseto` crate underneath and currently only supports PASETO Tokens V4.public.
pub(crate) mod claims;
pub mod errors;
pub(crate) mod maker;
pub use claims::Claims;
pub use maker::Maker;
pub mod purpose;
pub mod version;

311
src/maker/mod.rs Normal file
View File

@ -0,0 +1,311 @@
#![allow(dead_code)]
use std::marker::PhantomData;
use rusty_paseto::{
core::{
Key, PasetoAsymmetricPrivateKey, PasetoAsymmetricPublicKey, Public as pPublic, V4 as pV4,
},
prelude::{
AudienceClaim, CustomClaim, ExpirationClaim, IssuedAtClaim, IssuerClaim, NotBeforeClaim,
PasetoBuilder, SubjectClaim, TokenIdentifierClaim,
},
};
use crate::{claims::reserved, purpose::Public, version::V4, Claims};
use crate::{
errors::{MakerError, TokenError},
purpose::Purpose,
version::Version,
};
// pub mod error;
pub struct Maker<V: Version, P: Purpose> {
private_key: Key<64>,
public_key: Key<32>,
public_key_bytes: [u8; 32],
version: String,
purpose: String,
_version: PhantomData<V>,
_purpose: PhantomData<P>,
}
/// `Maker` is a struct that provides functionality to create and manage PASETO (Platform-Agnostic Security Tokens) tokens.
///
/// # Methods
///
/// - `new(private_key: &[u8; 64]) -> Self`
/// - Creates a new `Maker` instance with the given private and public keys.
/// - `new_with_keypair() -> Self`
/// - Generates a new keypair and creates a new `Maker` instance with the generated keys.
/// - `new_keypair() -> ([u8; 64], [u8; 32])`
/// - Generates a new Ed25519 keypair and returns the private and public keys.
/// - `private_key(&self) -> PasetoAsymmetricPrivateKey<V4, Public>`
/// - Returns the private key as a `PasetoAsymmetricPrivateKey`.
/// - `public_key(&self) -> PasetoAsymmetricPublicKey<V4, Public>`
/// - Returns the public key as a `PasetoAsymmetricPublicKey`.
/// - `create_token(&self, claims: &Claims) -> Result<String, TokenError>`
/// - Creates a new PASETO token with the given claims. Returns the token as a `String` or an error if the token creation fails.
///
/// # Example
///
/// ```rust
/// use paseto_maker::{Maker, Claims, version::V4, purpose::Public};
/// let maker = Maker::new_with_keypair().unwrap();
/// let claims = Claims::new();
/// let token = maker.create_token(&claims).unwrap();
/// ```
impl Maker<V4, Public> {
/// # Errors
///
/// This function will return an error if the provided private key is invalid.
pub fn new(private_key: &[u8; 64]) -> Result<Self, MakerError> {
let private_key = ed25519_dalek::SigningKey::from_keypair_bytes(private_key)
.map_err(|err| MakerError::InvalidKey(err.to_string()))?;
let public_key = private_key.verifying_key().to_bytes();
Ok(Self {
private_key: Key::<64>::from(&private_key.to_keypair_bytes()),
public_key: Key::<32>::from(&public_key),
public_key_bytes: public_key,
version: V4::NAME.to_string(),
purpose: Public::NAME.to_string(),
_version: PhantomData,
_purpose: PhantomData,
})
}
/// # Errors
///
/// This function will return an error if the key generation or Maker creation fails.
pub fn new_with_keypair() -> Result<Self, MakerError> {
// let (private_key, public_key) = Self::new_keypair();
let private_key = Self::new_private_key();
Self::new(&private_key)
}
#[must_use]
pub fn new_private_key() -> [u8; 64] {
let mut csprng = rand::rngs::OsRng;
let priv_key: ed25519_dalek::SigningKey = ed25519_dalek::SigningKey::generate(&mut csprng);
priv_key.to_keypair_bytes()
}
#[must_use]
pub fn new_keypair() -> ([u8; 64], [u8; 32]) {
let mut csprng = rand::rngs::OsRng;
let priv_key: ed25519_dalek::SigningKey = ed25519_dalek::SigningKey::generate(&mut csprng);
let pub_key = priv_key.verifying_key();
(priv_key.to_keypair_bytes(), pub_key.to_bytes())
}
fn private_key(&self) -> PasetoAsymmetricPrivateKey<pV4, pPublic> {
PasetoAsymmetricPrivateKey::<pV4, pPublic>::from(&self.private_key)
}
#[must_use]
pub fn public_key(&self) -> PasetoAsymmetricPublicKey<pV4, pPublic> {
PasetoAsymmetricPublicKey::<pV4, pPublic>::from(&self.public_key)
}
#[must_use]
pub const fn public_key_as_bytes(&self) -> &[u8; 32] {
&self.public_key_bytes
}
/// # Errors
///
/// This function will return an error if the token verification fails.
pub fn verify_token(&self, token: &str) -> Result<Claims, TokenError> {
let public_key = self.public_key();
let mut parser = rusty_paseto::prelude::PasetoParser::<pV4, pPublic>::default();
let token = {
parser
.parse(token, &public_key)
.map_err(|err| TokenError::TokenCreationFailed(err.to_string()))?
};
Ok(token.into())
}
/// # Errors
///
/// This function will return an error if the token creation fails due to invalid claims or other issues.
///
pub fn create_token(&self, claims: &Claims) -> Result<String, TokenError> {
let mut builder = PasetoBuilder::<pV4, pPublic>::default();
for (key, value) in claims.iter() {
// dbg!(key, format!("{}", value.to_string().trim_matches('"').to_string()));
match key.as_ref() {
reserved::ISSUER => {
if let Some(issuer) = value.as_str() {
let _ = builder.set_claim(IssuerClaim::from(issuer));
} else {
return Err(TokenError::InvalidClaim("Invalid issuer claim".to_string()));
}
}
reserved::AUDIENCE => {
if let Some(audience) = value.as_str() {
let _ = builder.set_claim(AudienceClaim::from(audience));
} else {
return Err(TokenError::InvalidClaim(
"Invalid audience claim".to_string(),
));
}
}
reserved::SUBJECT => {
if let Some(subject) = value.as_str() {
let _ = builder.set_claim(SubjectClaim::from(subject));
} else {
return Err(TokenError::InvalidClaim(
"Invalid subject claim".to_string(),
));
}
}
reserved::ISSUED_AT => {
if let Some(issued_at) = value.as_str() {
match IssuedAtClaim::try_from(issued_at) {
Ok(claim) => {
let _ = builder.set_claim(claim);
}
Err(err) => {
return Err(TokenError::ClaimError(err.into()));
}
}
} else {
return Err(TokenError::InvalidClaim(
"Invalid issued at claim".to_string(),
));
}
}
reserved::NOT_BEFORE => {
if let Some(not_before) = value.as_str() {
match NotBeforeClaim::try_from(not_before) {
Ok(claim) => {
let _ = builder.set_claim(claim);
}
Err(err) => {
return Err(TokenError::ClaimError(err.into()));
}
}
} else {
return Err(TokenError::InvalidClaim(
"Invalid not before claim".to_string(),
));
}
}
reserved::EXPIRATION => {
if let Some(expiration) = value.as_str() {
match ExpirationClaim::try_from(expiration) {
Ok(claim) => {
let _ = builder.set_claim(claim);
}
Err(err) => {
return Err(TokenError::ClaimError(err.into()));
}
}
} else {
return Err(TokenError::InvalidClaim(
"Invalid expiration claim".to_string(),
));
}
}
reserved::TOKEN_IDENTIFIER => {
let claim = match value.as_str() {
Some(token_id) => TokenIdentifierClaim::from(token_id),
None => {
return Err(TokenError::InvalidClaim(
"Invalid token identifier claim".to_string(),
))
}
};
let _ = builder.set_claim(claim);
}
key => match CustomClaim::try_from((key, value)) {
Ok(claim) => {
let _ = builder.set_claim(claim);
}
Err(err) => {
return Err(TokenError::InvalidClaim(err.to_string()));
}
},
}
}
builder
.build(&self.private_key())
.map_err(|err| TokenError::TokenCreationFailed(err.to_string()))
}
}
#[cfg(test)]
mod test {
use std::{
fs::File,
io::{Read, Write},
};
use rusty_paseto::prelude::PasetoParser;
use super::*;
#[test]
fn test_invalid_claims() {
let maker = Maker::new_with_keypair().expect("failed to create maker");
let claims = Claims::new().with_issued_at("invalid RF3339 date");
let token = maker.create_token(&claims);
assert!(token.is_err());
}
#[test]
fn test_create_token() {
let maker = Maker::new_with_keypair().expect("failed to create maker");
let public_key = maker.public_key();
let mut claims = Claims::new().with_issued_at("2027-09-18T03:42:15+02:00");
claims.set_claim("sub", "this is the subject").unwrap();
claims.set_claim("data", "test").unwrap();
claims.set_claim("number", 2).unwrap();
let token = maker
.create_token(&claims)
.expect("failed to generate token");
let got = maker.verify_token(&token).expect("failed to verify token");
assert_eq!(got.get_subject().unwrap().as_str(), "this is the subject");
let mut parser = PasetoParser::<pV4, pPublic>::default();
let token = parser
.parse(&token, &public_key)
.expect("failed to parse token");
dbg!(&token);
assert!(token.get("sub").is_some());
assert_eq!(
token.get("sub").unwrap().as_str().unwrap(),
"this is the subject"
);
assert!(token.get("number").is_some());
assert_eq!(token.get("number").unwrap(), 2);
assert!(token.get("data").is_some());
assert_eq!(token.get("data").unwrap(), "test");
}
#[test]
fn test_new_private_key() {
let new_key = Key::<64>::try_new_random().unwrap();
// let private_key = PasetoAsymmetricPrivateKey::<V4, Public>::from(&new_key);
let mut file = File::create("temp_dev_private_key").unwrap();
file.write_all(new_key.as_slice()).unwrap();
let mut file = File::open("temp_dev_private_key").unwrap();
let mut private_key = Vec::new();
file.read_to_end(&mut private_key).unwrap();
let got_key = Key::<64>::from(private_key.as_slice());
assert_eq!(got_key.as_slice(), new_key.as_slice());
std::fs::remove_file("temp_dev_private_key").unwrap();
}
}

3
src/purpose/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub trait Purpose {}
mod public;
pub use public::Public;

8
src/purpose/public.rs Normal file
View File

@ -0,0 +1,8 @@
use super::Purpose;
pub struct Public;
impl Public {
pub const NAME: &'static str = "public";
}
impl Purpose for Public {}

3
src/version/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub trait Version {}
mod v4;
pub use v4::V4;

7
src/version/v4.rs Normal file
View File

@ -0,0 +1,7 @@
use super::Version;
pub struct V4;
impl V4 {
pub const NAME: &'static str = "v4";
}
impl Version for V4 {}