Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Appstoreconnect #54

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 82 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
'apple-flat-package',
'apple-sdk',
'apple-xar',
'app-store-connect',
'cpio-archive',
]
resolver = "2"
22 changes: 22 additions & 0 deletions app-store-connect/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "app-store-connect"
version = "0.1.0"
edition = "2021"
license = "Apache-2.0 OR MIT"

[dependencies]
anyhow = "1.0.68"
base64 = "0.20.0"
clap = { version = "4.0.29", features = ["derive"] }
dirs = "4.0.0"
env_logger = "0.10.0"
jsonwebtoken = "8.2.0"
log = "0.4.17"
pem = "1.1.0"
rand = "0.8.5"
reqwest = { version = "0.11.13", default-features = false, features = ["blocking", "json", "rustls-tls"] }
rsa = "0.7.2"
serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89"
thiserror = "1.0.38"
x509-certificate = "0.15.0"
132 changes: 132 additions & 0 deletions app-store-connect/src/api_key.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

//! API Key

use {
crate::{ConnectTokenEncoder, Error, Result},
anyhow::Context,
serde::{Deserialize, Serialize},
std::{fs::Permissions, io::Write, path::Path},
};

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

#[cfg(unix)]
fn set_permissions_private(p: &mut Permissions) {
p.set_mode(0o600);
}

#[cfg(windows)]
fn set_permissions_private(_: &mut Permissions) {}

/// Represents all metadata for an App Store Connect API Key.
///
/// This is a convenience type to aid in the generic representation of all the components
/// of an App Store Connect API Key. The type supports serialization so we save as a single
/// file or payload to enhance usability (so people don't need to provide all 3 pieces of the
/// API Key for all operations).
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct UnifiedApiKey {
/// Who issued the key.
///
/// Likely a UUID.
issuer_id: String,

/// Key identifier.
///
/// An alphanumeric string like `DEADBEEF42`.
key_id: String,

/// Base64 encoded DER of ECDSA private key material.
private_key: String,
}

impl UnifiedApiKey {
/// Construct an instance from constitute parts and a PEM encoded ECDSA private key.
///
/// This is what you want to use if importing a private key from the file downloaded
/// from the App Store Connect web interface.
pub fn from_ecdsa_pem_path(
issuer_id: impl ToString,
key_id: impl ToString,
path: impl AsRef<Path>,
) -> Result<Self> {
let pem_data = std::fs::read(path.as_ref())?;

let parsed = pem::parse(pem_data).map_err(|_| InvalidPemPrivateKey)?;

if parsed.tag != "PRIVATE KEY" {
return Err(InvalidPemPrivateKey.into());
}

let private_key = base64::encode(parsed.contents);

Ok(Self {
issuer_id: issuer_id.to_string(),
key_id: key_id.to_string(),
private_key,
})
}

/// Construct an instance from serialized JSON.
pub fn from_json(data: impl AsRef<[u8]>) -> Result<Self> {
Ok(serde_json::from_slice(data.as_ref())?)
}

/// Construct an instance from a JSON file.
pub fn from_json_path(path: impl AsRef<Path>) -> Result<Self> {
let data = std::fs::read(path.as_ref())?;

Self::from_json(data)
}

/// Serialize this instance to a JSON object.
pub fn to_json_string(&self) -> Result<String> {
Ok(serde_json::to_string_pretty(&self)?)
}

/// Write this instance to a JSON file.
///
/// Since the file contains sensitive data, it will have limited read permissions
/// on platforms where this is implemented. Parent directories will be created if missing
/// using default permissions for created directories.
///
/// Permissions on the resulting file may not be as restrictive as desired. It is up
/// to callers to additionally harden as desired.
pub fn write_json_file(&self, path: impl AsRef<Path>) -> Result<()> {
let path = path.as_ref();

if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}

let data = self.to_json_string()?;

let mut fh = std::fs::File::create(path)?;
let mut permissions = fh.metadata()?.permissions();
set_permissions_private(&mut permissions);
fh.set_permissions(permissions)?;
fh.write_all(data.as_bytes())?;

Ok(())
}
}

impl TryFrom<UnifiedApiKey> for ConnectTokenEncoder {
type Error = anyhow::Error;

fn try_from(value: UnifiedApiKey) -> Result<Self> {
let der = base64::decode(value.private_key).context("invalid unified api key")?;

Self::from_ecdsa_der(value.key_id, value.issuer_id, &der)
}
}

#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)]
#[error("invalid PEM formatted private key")]
pub struct InvalidPemPrivateKey;
Loading