box-open-sdk 0.1.1

Generated Box API SDK for Rust (community, unofficial).
Documentation
// Code generated by box-gantry (vendored from runtimes/rust/gantryruntime/src/jwt.rs). DO NOT EDIT.

//! JWT server auth: the signing-key assertion flow (Box's `box_config.json`).
//!
//! The RSA private key is parsed (and, if encrypted, decrypted) up front so a
//! bad key fails loudly at construction rather than on the first request. Each
//! token refresh RS256-signs a short-lived, single-use JWT bearer assertion
//! that [`super::Auth::jwt`] exchanges at Box's token endpoint.

use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
use rsa::pkcs1::DecodeRsaPrivateKey as _;
use rsa::pkcs1v15::Pkcs1v15Sign;
use rsa::pkcs8::DecodePrivateKey as _;
use rsa::RsaPrivateKey;
use sha2::{Digest as _, Sha256};

use super::Error;

/// JWT server auth config — the fields Box's `box_config.json` carries. Set
/// exactly one subject: `enterprise_id` for the service account, or `user_id`
/// to act as a managed user.
pub struct JwtConfig {
    pub client_id: String,
    pub client_secret: String,
    /// The `publicKeyID` from the app's `box_config.json`.
    pub public_key_id: String,
    /// The RSA private key PEM (optionally passphrase-encrypted).
    pub private_key_pem: Vec<u8>,
    /// The passphrase for an encrypted `private_key_pem`, if any.
    pub passphrase: Option<String>,
    pub enterprise_id: String,
    /// Optional: act as a managed user instead of the enterprise service account.
    pub user_id: Option<String>,
    /// Optional: defaults to Box's token endpoint (custom deployments).
    pub token_url: Option<String>,
}

/// Monotonic tiebreaker so two assertions minted in the same nanosecond still
/// get distinct `jti`s (Box requires each assertion be single-use).
static JTI_COUNTER: AtomicU64 = AtomicU64::new(0);

/// A parsed signing key plus the immutable claim inputs. Re-used across
/// refreshes; each `assertion` call mints a fresh, single-use JWT.
pub(crate) struct Signer {
    key: RsaPrivateKey,
    client_id: String,
    public_key_id: String,
    subject_type: &'static str,
    subject_id: String,
}

impl Signer {
    /// Parse (and if needed decrypt) the RSA private key up front, so a bad key
    /// is a construction error, not a first-request surprise.
    pub(crate) fn new(config: &JwtConfig) -> Result<Signer, Error> {
        let key = parse_rsa_private_key(&config.private_key_pem, config.passphrase.as_deref())?;
        let (subject_type, subject_id) = match &config.user_id {
            Some(user) => ("user", user.clone()),
            None => ("enterprise", config.enterprise_id.clone()),
        };
        Ok(Signer {
            key,
            client_id: config.client_id.clone(),
            public_key_id: config.public_key_id.clone(),
            subject_type,
            subject_id,
        })
    }

    /// Build and RS256-sign the JWT bearer assertion for `audience` (the token
    /// endpoint). The claim set is single-use: a fresh `jti` and a 45s expiry.
    pub(crate) fn assertion(&self, audience: &str) -> Result<String, Error> {
        let header = serde_json::json!({
            "alg": "RS256",
            "typ": "JWT",
            "kid": self.public_key_id,
        });
        let claims = serde_json::json!({
            "iss": self.client_id,
            "sub": self.subject_id,
            "box_sub_type": self.subject_type,
            "aud": audience,
            "jti": jti(),
            "exp": now_unix() + 45,
        });
        let signing_input = format!(
            "{}.{}",
            b64(&serde_json::to_vec(&header)?),
            b64(&serde_json::to_vec(&claims)?)
        );
        let digest = Sha256::digest(signing_input.as_bytes());
        let signature = self
            .key
            .sign(Pkcs1v15Sign::new::<Sha256>(), &digest)
            .map_err(|e| Error::new(format!("gantryruntime: signing assertion: {e}")))?;
        Ok(format!("{signing_input}.{}", b64(&signature)))
    }
}

/// Decode a PEM RSA key: encrypted PKCS#8 when a passphrase is present (Box's
/// `box_config` keys), else unencrypted PKCS#8 or PKCS#1.
fn parse_rsa_private_key(pem: &[u8], passphrase: Option<&str>) -> Result<RsaPrivateKey, Error> {
    let text = std::str::from_utf8(pem)
        .map_err(|_| Error::new("gantryruntime: private key PEM is not valid UTF-8"))?;
    if text.contains("ENCRYPTED PRIVATE KEY") {
        let passphrase = passphrase.ok_or_else(|| {
            Error::new("gantryruntime: private key is encrypted but no passphrase was given")
        })?;
        return RsaPrivateKey::from_pkcs8_encrypted_pem(text, passphrase.as_bytes())
            .map_err(|e| Error::new(format!("gantryruntime: decrypting private key: {e}")));
    }
    if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(text) {
        return Ok(key);
    }
    RsaPrivateKey::from_pkcs1_pem(text)
        .map_err(|e| Error::new(format!("gantryruntime: parsing private key: {e}")))
}

/// URL-safe base64 without padding (JWS `BASE64URL`).
fn b64(bytes: &[u8]) -> String {
    URL_SAFE_NO_PAD.encode(bytes)
}

/// Seconds since the Unix epoch.
fn now_unix() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0)
}

/// A single-use assertion id: the current nanoseconds plus a monotonic counter,
/// so rapid successive assertions never collide.
fn jti() -> String {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    let counter = JTI_COUNTER.fetch_add(1, Ordering::Relaxed);
    format!("{nanos:032x}{counter:016x}")
}