Skip to main content

box_open_sdk/runtime/
jwt.rs

1// Code generated by box-gantry (vendored from runtimes/rust/gantryruntime/src/jwt.rs). DO NOT EDIT.
2
3//! JWT server auth: the signing-key assertion flow (Box's `box_config.json`).
4//!
5//! The RSA private key is parsed (and, if encrypted, decrypted) up front so a
6//! bad key fails loudly at construction rather than on the first request. Each
7//! token refresh RS256-signs a short-lived, single-use JWT bearer assertion
8//! that [`super::Auth::jwt`] exchanges at Box's token endpoint.
9
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use base64::engine::general_purpose::URL_SAFE_NO_PAD;
14use base64::Engine as _;
15use rsa::pkcs1::DecodeRsaPrivateKey as _;
16use rsa::pkcs1v15::Pkcs1v15Sign;
17use rsa::pkcs8::DecodePrivateKey as _;
18use rsa::RsaPrivateKey;
19use sha2::{Digest as _, Sha256};
20
21use super::Error;
22
23/// JWT server auth config — the fields Box's `box_config.json` carries. Set
24/// exactly one subject: `enterprise_id` for the service account, or `user_id`
25/// to act as a managed user.
26pub struct JwtConfig {
27    pub client_id: String,
28    pub client_secret: String,
29    /// The `publicKeyID` from the app's `box_config.json`.
30    pub public_key_id: String,
31    /// The RSA private key PEM (optionally passphrase-encrypted).
32    pub private_key_pem: Vec<u8>,
33    /// The passphrase for an encrypted `private_key_pem`, if any.
34    pub passphrase: Option<String>,
35    pub enterprise_id: String,
36    /// Optional: act as a managed user instead of the enterprise service account.
37    pub user_id: Option<String>,
38    /// Optional: defaults to Box's token endpoint (custom deployments).
39    pub token_url: Option<String>,
40}
41
42/// Monotonic tiebreaker so two assertions minted in the same nanosecond still
43/// get distinct `jti`s (Box requires each assertion be single-use).
44static JTI_COUNTER: AtomicU64 = AtomicU64::new(0);
45
46/// A parsed signing key plus the immutable claim inputs. Re-used across
47/// refreshes; each `assertion` call mints a fresh, single-use JWT.
48pub(crate) struct Signer {
49    key: RsaPrivateKey,
50    client_id: String,
51    public_key_id: String,
52    subject_type: &'static str,
53    subject_id: String,
54}
55
56impl Signer {
57    /// Parse (and if needed decrypt) the RSA private key up front, so a bad key
58    /// is a construction error, not a first-request surprise.
59    pub(crate) fn new(config: &JwtConfig) -> Result<Signer, Error> {
60        let key = parse_rsa_private_key(&config.private_key_pem, config.passphrase.as_deref())?;
61        let (subject_type, subject_id) = match &config.user_id {
62            Some(user) => ("user", user.clone()),
63            None => ("enterprise", config.enterprise_id.clone()),
64        };
65        Ok(Signer {
66            key,
67            client_id: config.client_id.clone(),
68            public_key_id: config.public_key_id.clone(),
69            subject_type,
70            subject_id,
71        })
72    }
73
74    /// Build and RS256-sign the JWT bearer assertion for `audience` (the token
75    /// endpoint). The claim set is single-use: a fresh `jti` and a 45s expiry.
76    pub(crate) fn assertion(&self, audience: &str) -> Result<String, Error> {
77        let header = serde_json::json!({
78            "alg": "RS256",
79            "typ": "JWT",
80            "kid": self.public_key_id,
81        });
82        let claims = serde_json::json!({
83            "iss": self.client_id,
84            "sub": self.subject_id,
85            "box_sub_type": self.subject_type,
86            "aud": audience,
87            "jti": jti(),
88            "exp": now_unix() + 45,
89        });
90        let signing_input = format!(
91            "{}.{}",
92            b64(&serde_json::to_vec(&header)?),
93            b64(&serde_json::to_vec(&claims)?)
94        );
95        let digest = Sha256::digest(signing_input.as_bytes());
96        let signature = self
97            .key
98            .sign(Pkcs1v15Sign::new::<Sha256>(), &digest)
99            .map_err(|e| Error::new(format!("gantryruntime: signing assertion: {e}")))?;
100        Ok(format!("{signing_input}.{}", b64(&signature)))
101    }
102}
103
104/// Decode a PEM RSA key: encrypted PKCS#8 when a passphrase is present (Box's
105/// `box_config` keys), else unencrypted PKCS#8 or PKCS#1.
106fn parse_rsa_private_key(pem: &[u8], passphrase: Option<&str>) -> Result<RsaPrivateKey, Error> {
107    let text = std::str::from_utf8(pem)
108        .map_err(|_| Error::new("gantryruntime: private key PEM is not valid UTF-8"))?;
109    if text.contains("ENCRYPTED PRIVATE KEY") {
110        let passphrase = passphrase.ok_or_else(|| {
111            Error::new("gantryruntime: private key is encrypted but no passphrase was given")
112        })?;
113        return RsaPrivateKey::from_pkcs8_encrypted_pem(text, passphrase.as_bytes())
114            .map_err(|e| Error::new(format!("gantryruntime: decrypting private key: {e}")));
115    }
116    if let Ok(key) = RsaPrivateKey::from_pkcs8_pem(text) {
117        return Ok(key);
118    }
119    RsaPrivateKey::from_pkcs1_pem(text)
120        .map_err(|e| Error::new(format!("gantryruntime: parsing private key: {e}")))
121}
122
123/// URL-safe base64 without padding (JWS `BASE64URL`).
124fn b64(bytes: &[u8]) -> String {
125    URL_SAFE_NO_PAD.encode(bytes)
126}
127
128/// Seconds since the Unix epoch.
129fn now_unix() -> u64 {
130    SystemTime::now()
131        .duration_since(UNIX_EPOCH)
132        .map(|d| d.as_secs())
133        .unwrap_or(0)
134}
135
136/// A single-use assertion id: the current nanoseconds plus a monotonic counter,
137/// so rapid successive assertions never collide.
138fn jti() -> String {
139    let nanos = SystemTime::now()
140        .duration_since(UNIX_EPOCH)
141        .map(|d| d.as_nanos())
142        .unwrap_or(0);
143    let counter = JTI_COUNTER.fetch_add(1, Ordering::Relaxed);
144    format!("{nanos:032x}{counter:016x}")
145}