diff --git a/.gitignore b/.gitignore index d01bd1a..e2de126 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ \ No newline at end of file +#.idea/ + +# Added by cargo + +/target +.vscode/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f5a746f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "paseto_maker" +authors = ["itsscb "] +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" diff --git a/README.md b/README.md index feacf7f..9592348 100644 --- a/README.md +++ b/README.md @@ -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> { + 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. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c34067c --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a70fa45 --- /dev/null +++ b/flake.nix @@ -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 ]); + }; + } + ); +} diff --git a/src/claims/mod.rs b/src/claims/mod.rs new file mode 100644 index 0000000..2399f02 --- /dev/null +++ b/src/claims/mod.rs @@ -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` 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 = 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 = claims.get_claim("sub"); +/// assert_eq!(sub, Some("1234567890".to_string())); +/// +/// let name: Option = claims.get_claim("name"); +/// assert_eq!(name, Some("John Doe".to_string())); +/// +/// let admin: Option = 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 = claims.get_subject(); +/// assert_eq!(subject, Some("1234567890".to_string())); +/// +/// let issuer: Option = claims.get_issuer(); +/// assert_eq!(issuer, Some("issuer".to_string())); +/// +/// let audience: Option = claims.get_audience(); +/// assert_eq!(audience, Some("audience".to_string())); +/// +/// let expiration: Option> = claims.get_expiration(); +/// assert_eq!(expiration.unwrap().to_rfc3339().to_string(), "2023-10-01T00:00:00+00:00".to_string()); +/// +/// let not_before: Option> = 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> = 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 = claims.get_token_identifier(); +/// assert_eq!(token_identifier, Some("token_id".to_string())); +/// ``` + +#[derive(Debug, Default)] +pub struct Claims { + claims: BTreeMap, Value>, +} + +impl Display for Claims { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let claims: Vec = self + .claims + .iter() + .map(|(k, v)| format!("{k}: {v}")) + .collect(); + write!(f, "{}", claims.join(", ")) + } +} + +impl From for Claims { + fn from(value: Value) -> Self { + let claims: BTreeMap, 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, &Value)> { + self.claims.iter() + } + + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn with_subject>(mut self, subject: T) -> Self { + self.claims + .insert(Arc::from(reserved::SUBJECT), subject.as_ref().into()); + self + } + + #[must_use] + pub fn with_issuer>(mut self, issuer: T) -> Self { + self.claims + .insert(Arc::from(reserved::ISSUER), issuer.as_ref().into()); + self + } + + #[must_use] + pub fn with_audience>(mut self, audience: T) -> Self { + self.claims + .insert(Arc::from(reserved::AUDIENCE), audience.as_ref().into()); + self + } + + #[must_use] + pub fn with_expiration>(mut self, expiration: T) -> Self { + self.claims + .insert(Arc::from(reserved::EXPIRATION), expiration.as_ref().into()); + self + } + + #[must_use] + pub fn with_not_before>(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>(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>(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(&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(&self, key: &str) -> Option { + self.claims + .get(&Arc::from(key)) + .and_then(|value| serde_json::from_value(value.clone()).ok()) + } + + #[must_use] + pub fn get_subject(&self) -> Option { + self.get_claim(reserved::SUBJECT) + } + + #[must_use] + pub fn get_issuer(&self) -> Option { + self.get_claim(reserved::ISSUER) + } + + #[must_use] + pub fn get_audience(&self) -> Option { + self.get_claim(reserved::AUDIENCE) + } + + #[must_use] + pub fn get_expiration(&self) -> Option> { + self.get_claim(reserved::EXPIRATION) + } + + #[must_use] + pub fn get_not_before(&self) -> Option> { + self.get_claim(reserved::NOT_BEFORE) + } + + #[must_use] + pub fn get_issued_at(&self) -> Option> { + self.get_claim(reserved::ISSUED_AT) + } + + #[must_use] + pub fn get_token_identifier(&self) -> Option { + 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 = claims.get_claim("sub"); + assert_eq!(sub, Some("1234567890".to_string())); + + let name: Option = claims.get_claim("name"); + assert_eq!(name, Some("John Doe".to_string())); + + let admin: Option = 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 = claims.get_claim("sub"); + assert_eq!(sub, Some("1234567890".to_string())); + let sub: Option = claims.get_claim("aud"); + assert_eq!(sub, Some("test audience".to_string())); + + let sub: Option = 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 = 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()); + } +} diff --git a/src/claims/reserved.rs b/src/claims/reserved.rs new file mode 100644 index 0000000..3c683a0 --- /dev/null +++ b/src/claims/reserved.rs @@ -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"; diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..9d64758 --- /dev/null +++ b/src/errors.rs @@ -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), +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f9f9411 --- /dev/null +++ b/src/lib.rs @@ -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> { +//! 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; diff --git a/src/maker/mod.rs b/src/maker/mod.rs new file mode 100644 index 0000000..7e190a2 --- /dev/null +++ b/src/maker/mod.rs @@ -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 { + private_key: Key<64>, + public_key: Key<32>, + public_key_bytes: [u8; 32], + version: String, + purpose: String, + _version: PhantomData, + _purpose: PhantomData

, +} + +/// `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` +/// - Returns the private key as a `PasetoAsymmetricPrivateKey`. +/// - `public_key(&self) -> PasetoAsymmetricPublicKey` +/// - Returns the public key as a `PasetoAsymmetricPublicKey`. +/// - `create_token(&self, claims: &Claims) -> Result` +/// - 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 { + /// # Errors + /// + /// This function will return an error if the provided private key is invalid. + pub fn new(private_key: &[u8; 64]) -> Result { + 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 { + // 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 { + PasetoAsymmetricPrivateKey::::from(&self.private_key) + } + + #[must_use] + pub fn public_key(&self) -> PasetoAsymmetricPublicKey { + PasetoAsymmetricPublicKey::::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 { + let public_key = self.public_key(); + let mut parser = rusty_paseto::prelude::PasetoParser::::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 { + let mut builder = PasetoBuilder::::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::::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::::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(); + } +} diff --git a/src/purpose/mod.rs b/src/purpose/mod.rs new file mode 100644 index 0000000..cb50d80 --- /dev/null +++ b/src/purpose/mod.rs @@ -0,0 +1,3 @@ +pub trait Purpose {} +mod public; +pub use public::Public; diff --git a/src/purpose/public.rs b/src/purpose/public.rs new file mode 100644 index 0000000..dbc4cd0 --- /dev/null +++ b/src/purpose/public.rs @@ -0,0 +1,8 @@ +use super::Purpose; + +pub struct Public; +impl Public { + pub const NAME: &'static str = "public"; +} + +impl Purpose for Public {} diff --git a/src/version/mod.rs b/src/version/mod.rs new file mode 100644 index 0000000..8a68e9f --- /dev/null +++ b/src/version/mod.rs @@ -0,0 +1,3 @@ +pub trait Version {} +mod v4; +pub use v4::V4; diff --git a/src/version/v4.rs b/src/version/v4.rs new file mode 100644 index 0000000..0f1396e --- /dev/null +++ b/src/version/v4.rs @@ -0,0 +1,7 @@ +use super::Version; + +pub struct V4; +impl V4 { + pub const NAME: &'static str = "v4"; +} +impl Version for V4 {}