use {
crate::Result,
jsonwebtoken::{Algorithm, EncodingKey, Header},
serde::{Deserialize, Serialize},
std::{path::Path, time::SystemTime},
thiserror::Error,
};
#[derive(Clone, Debug, Deserialize, Serialize)]
struct ConnectTokenRequest {
iss: String,
iat: u64,
exp: u64,
aud: String,
}
pub type AppStoreConnectToken = String;
#[derive(Clone)]
pub struct ConnectTokenEncoder {
key_id: String,
issuer_id: String,
encoding_key: EncodingKey,
}
impl ConnectTokenEncoder {
pub fn from_jwt_encoding_key(
key_id: String,
issuer_id: String,
encoding_key: EncodingKey,
) -> Self {
Self {
key_id,
issuer_id,
encoding_key,
}
}
pub fn from_ecdsa_der(key_id: String, issuer_id: String, der_data: &[u8]) -> Result<Self> {
let encoding_key = EncodingKey::from_ec_der(der_data);
Ok(Self::from_jwt_encoding_key(key_id, issuer_id, encoding_key))
}
pub fn from_ecdsa_pem(key_id: String, issuer_id: String, pem_data: &[u8]) -> Result<Self> {
let encoding_key = EncodingKey::from_ec_pem(pem_data)?;
Ok(Self::from_jwt_encoding_key(key_id, issuer_id, encoding_key))
}
pub fn from_ecdsa_pem_path(
key_id: String,
issuer_id: String,
path: impl AsRef<Path>,
) -> Result<Self> {
let data = std::fs::read(path.as_ref())?;
Self::from_ecdsa_pem(key_id, issuer_id, &data)
}
pub fn from_api_key_id(key_id: String, issuer_id: String) -> Result<Self> {
let mut search_paths = vec![std::env::current_dir()?.join("private_keys")];
if let Some(home) = dirs::home_dir() {
search_paths.extend([
home.join("private_keys"),
home.join(".private_keys"),
home.join(".appstoreconnect").join("private_keys"),
]);
}
let filename = format!("AuthKey_{key_id}.p8");
for path in search_paths {
let candidate = path.join(filename.as_str());
if candidate.exists() {
return Self::from_ecdsa_pem_path(key_id, issuer_id, candidate);
}
}
Err(MissingApiKey.into())
}
pub fn new_token(&self, duration: u64) -> Result<AppStoreConnectToken> {
let header = Header {
kid: Some(self.key_id.clone()),
alg: Algorithm::ES256,
..Default::default()
};
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("calculating UNIX time should never fail")
.as_secs();
let claims = ConnectTokenRequest {
iss: self.issuer_id.clone(),
iat: now,
exp: now + duration,
aud: "appstoreconnect-v1".to_string(),
};
let token = jsonwebtoken::encode(&header, &claims, &self.encoding_key)?;
Ok(token)
}
}
#[derive(Clone, Copy, Debug, Error)]
#[error("no app store connect api key found")]
pub struct MissingApiKey;