Security

PASETO

PASETO v4 tokens, the safer JWT: v4.local encrypted with a shared key and v4.public signed with Ed25519, expiry checked.

Version 1.0.0 · 6 functions · 2 crates

#auth#tokens#sessions#api

Functions

What use paseto.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use paseto_local_key, paseto_encrypt from paseto (Quillo 0.6).

paseto_local_key() → stror an error

A new key for v4.local tokens: 32 random bytes as base64. Everyone who has it can make and read tokens.

paseto_encrypt(claims_map: any, key: str, expires_in: int) → stror an error

A v4.local. token holding claims encrypted with key: nobody without the key can read or change them. iat and nbf are set to now; exp to now plus expires_in seconds (0: never expires). iss, sub, aud, jti are text.

paseto_decrypt(token: str, key: str) → anyor an error

The claims of a v4.local token made with key; an error for another key, a changed token, or one that has expired.

paseto_keypair() → mapor an error

A new Ed25519 key pair for v4.public tokens: { secret, public }, base64 (64 and 32 bytes). Sign with secret; give public to the services that check.

paseto_sign(claims_map: any, secret: str, expires_in: int) → stror an error

A v4.public. token: claims readable by anyone, signed with secret; exp as paseto_encrypt sets it.

paseto_verify(token: str, public_key: str) → anyor an error

The claims of a v4.public token, checked with public_key; an error for a wrong signature or an expired token.

Get it

$ quillo install paseto               # paseto.qwer into this folder$ quillo install paseto --example     # and its example, paseto.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/paseto.qwer$ curl -O https://quillolang.org/plugins/files/paseto.qwe

Then, in your program, all of it or only what you call:

main.qwe
use paseto.qweruse paseto_local_key, paseto_encrypt from paseto      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
pasetors = "0.7"base64 = "0.22"

Example

paseto.qwe
// Sign-in tokens with PASETO: an encrypted session token, and a signed one other services check.////   quillo run paseto.qwe      then open http://localhost:8080/sessionuse "paseto.qwer"let LOCAL_KEY = env("PASETO_KEY") ?? paseto_local_key()?let SIGNING = paseto_keypair()?route GET /session {    let token = paseto_encrypt({ sub: "user-7", role: "admin", cart: [12, 31] }, LOCAL_KEY, 3600)?    let claims = paseto_decrypt(token, LOCAL_KEY)?    { token, user: claims.sub, role: claims.role, cart: claims.cart, other_key: is_err(paseto_decrypt(token, paseto_local_key()?)) }}route GET /public {    let token = paseto_sign({ sub: "service-billing", aud: "orders-api", scope: "orders:read" }, SIGNING.secret, 300)?    let claims = paseto_verify(token, SIGNING.public)?    { prefix: slice(token, 0, 10), service: claims.sub, scope: claims.scope, expires: claims.exp, forged: is_err(paseto_verify(token, paseto_keypair()?.public)) }}
$ quillo run paseto.qwe$ curl 'localhost:8080/session'$ curl 'localhost:8080/public'

Source

paseto.qwer
//! PASETO v4 tokens, the safer JWT: `v4.local` encrypted with a shared key and `v4.public` signed with Ed25519, with expiry checked, for sessions and APIs.//! version: 1.0.0//!//! [dependencies]//! pasetors = "0.7"//! base64 = "0.22"use base64::engine::general_purpose::STANDARD;use base64::Engine;use pasetors::claims::{Claims, ClaimsValidationRules};use pasetors::keys::{AsymmetricKeyPair, AsymmetricPublicKey, AsymmetricSecretKey, Generate, SymmetricKey};use pasetors::token::UntrustedToken;use pasetors::version4::V4;use pasetors::{local, public, Local, Public};use serde_json::Value;use std::collections::HashMap;fn key_bytes(text: &str, len: usize, what: &str) -> Result<Vec<u8>, String> {    let b = STANDARD.decode(text.trim()).map_err(|_| format!("the {} is not base64", what))?;    if b.len() != len {        return Err(format!("the {} is {} bytes, not {}", what, len, b.len()));    }    Ok(b)}fn claims(values: Value, expires_in: i64) -> Result<Claims, String> {    let Value::Object(map) = values else { return Err("the claims are a map".into()) };    let mut c = Claims::new().map_err(|e| e.to_string())?;    if expires_in > 0 {        c.set_expires_in(&std::time::Duration::from_secs(expires_in.min(10 * 365 * 86400) as u64)).map_err(|e| e.to_string())?;    } else {        c.non_expiring();    }    for (k, v) in map {        let text = || v.as_str().map(str::to_string).ok_or_else(|| format!("`{}` is text", k));        let r = match k.as_str() {            "iss" => c.issuer(&text()?),            "sub" => c.subject(&text()?),            "aud" => c.audience(&text()?),            "jti" => c.token_identifier(&text()?),            "exp" => c.expiration(&text()?),            "nbf" => c.not_before(&text()?),            "iat" => c.issued_at(&text()?),            _ => c.add_additional(&k, v.clone()),        };        r.map_err(|e| format!("claim `{}`: {}", k, e))?;    }    Ok(c)}fn rules() -> ClaimsValidationRules {    let mut r = ClaimsValidationRules::new();    r.allow_non_expiring();    r}fn payload(c: Option<&Claims>) -> Result<Value, String> {    let c = c.ok_or("the token has no claims")?;    serde_json::from_str(&c.to_string().map_err(|e| e.to_string())?).map_err(|e| e.to_string())}/// A new key for v4.local tokens: 32 random bytes as base64. Everyone who has it can make and read tokens.pub fn paseto_local_key() -> Result<String, String> {    Ok(STANDARD.encode(SymmetricKey::<V4>::generate().map_err(|e| e.to_string())?.as_bytes()))}/// A `v4.local.` token holding `claims` encrypted with `key`: nobody without the key can read or change them. `iat` and `nbf`/// are set to now; `exp` to now plus `expires_in` seconds (0: never expires). `iss`, `sub`, `aud`, `jti` are text.pub fn paseto_encrypt(claims_map: serde_json::Value, key: &str, expires_in: i64) -> Result<String, String> {    let k = SymmetricKey::<V4>::from(&key_bytes(key, 32, "key")?).map_err(|e| e.to_string())?;    local::encrypt(&k, &claims(claims_map, expires_in)?, None, None).map_err(|e| e.to_string())}/// The claims of a v4.local token made with `key`; an error for another key, a changed token, or one that has expired.pub fn paseto_decrypt(token: &str, key: &str) -> Result<Value, String> {    let k = SymmetricKey::<V4>::from(&key_bytes(key, 32, "key")?).map_err(|e| e.to_string())?;    let t = UntrustedToken::<Local, V4>::try_from(token.trim()).map_err(|e| format!("not a v4.local token: {}", e))?;    let trusted = local::decrypt(&k, &t, &rules(), None, None).map_err(|e| format!("{}: wrong key, changed or expired", e))?;    payload(trusted.payload_claims())}/// A new Ed25519 key pair for v4.public tokens: `{ secret, public }`, base64 (64 and 32 bytes). Sign with `secret`; give/// `public` to the services that check.pub fn paseto_keypair() -> Result<HashMap<String, String>, String> {    let kp = AsymmetricKeyPair::<V4>::generate().map_err(|e| e.to_string())?;    Ok(HashMap::from([        ("secret".to_string(), STANDARD.encode(kp.secret.as_bytes())),        ("public".to_string(), STANDARD.encode(kp.public.as_bytes())),    ]))}/// A `v4.public.` token: `claims` readable by anyone, signed with `secret`; `exp` as paseto_encrypt sets it.pub fn paseto_sign(claims_map: serde_json::Value, secret: &str, expires_in: i64) -> Result<String, String> {    let k = AsymmetricSecretKey::<V4>::from(&key_bytes(secret, 64, "secret key")?).map_err(|e| e.to_string())?;    public::sign(&k, &claims(claims_map, expires_in)?, None, None).map_err(|e| e.to_string())}/// The claims of a v4.public token, checked with `public_key`; an error for a wrong signature or an expired token.pub fn paseto_verify(token: &str, public_key: &str) -> Result<Value, String> {    let k = AsymmetricPublicKey::<V4>::from(&key_bytes(public_key, 32, "public key")?).map_err(|e| e.to_string())?;    let t = UntrustedToken::<Public, V4>::try_from(token.trim()).map_err(|e| format!("not a v4.public token: {}", e))?;    let trusted = public::verify(&k, &t, &rules(), None, None).map_err(|e| format!("{}: wrong key, changed or expired", e))?;    payload(trusted.payload_claims())}
Thread
79Useful? Like it, or add to its thread below.

Thread 0 posts

On PASETO: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.