app_store_connect/
api_key.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7//! API Key
8
9use {
10    crate::{ConnectTokenEncoder, Error, Result},
11    anyhow::Context,
12    base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine},
13    serde::{Deserialize, Serialize},
14    std::{fs::Permissions, io::Write, path::Path},
15};
16
17#[cfg(unix)]
18use std::os::unix::fs::PermissionsExt;
19
20#[cfg(unix)]
21fn set_permissions_private(p: &mut Permissions) {
22    p.set_mode(0o600);
23}
24
25#[cfg(windows)]
26fn set_permissions_private(_: &mut Permissions) {}
27
28/// Represents all metadata for an App Store Connect API Key.
29///
30/// This is a convenience type to aid in the generic representation of all the components
31/// of an App Store Connect API Key. The type supports serialization so we save as a single
32/// file or payload to enhance usability (so people don't need to provide all 3 pieces of the
33/// API Key for all operations).
34#[derive(Clone, Debug, Deserialize, Serialize)]
35pub struct UnifiedApiKey {
36    /// Who issued the key.
37    ///
38    /// Likely a UUID.
39    issuer_id: String,
40
41    /// Key identifier.
42    ///
43    /// An alphanumeric string like `DEADBEEF42`.
44    key_id: String,
45
46    /// Base64 encoded DER of ECDSA private key material.
47    private_key: String,
48}
49
50impl UnifiedApiKey {
51    /// Construct an instance from constitute parts and a PEM encoded ECDSA private key.
52    ///
53    /// This is what you want to use if importing a private key from the file downloaded
54    /// from the App Store Connect web interface.
55    pub fn from_ecdsa_pem_path(
56        issuer_id: impl ToString,
57        key_id: impl ToString,
58        path: impl AsRef<Path>,
59    ) -> Result<Self> {
60        let pem_data = std::fs::read(path.as_ref())?;
61
62        let parsed = pem::parse(pem_data).map_err(|_| InvalidPemPrivateKey)?;
63
64        if parsed.tag() != "PRIVATE KEY" {
65            return Err(InvalidPemPrivateKey.into());
66        }
67
68        let private_key = STANDARD_ENGINE.encode(parsed.contents());
69
70        Ok(Self {
71            issuer_id: issuer_id.to_string(),
72            key_id: key_id.to_string(),
73            private_key,
74        })
75    }
76
77    /// Construct an instance from serialized JSON.
78    pub fn from_json(data: impl AsRef<[u8]>) -> Result<Self> {
79        Ok(serde_json::from_slice(data.as_ref())?)
80    }
81
82    /// Construct an instance from a JSON file.
83    pub fn from_json_path(path: impl AsRef<Path>) -> Result<Self> {
84        let data = std::fs::read(path.as_ref())?;
85
86        Self::from_json(data)
87    }
88
89    /// Serialize this instance to a JSON object.
90    pub fn to_json_string(&self) -> Result<String> {
91        Ok(serde_json::to_string_pretty(&self)?)
92    }
93
94    /// Write this instance to a JSON file.
95    ///
96    /// Since the file contains sensitive data, it will have limited read permissions
97    /// on platforms where this is implemented. Parent directories will be created if missing
98    /// using default permissions for created directories.
99    ///
100    /// Permissions on the resulting file may not be as restrictive as desired. It is up
101    /// to callers to additionally harden as desired.
102    pub fn write_json_file(&self, path: impl AsRef<Path>) -> Result<()> {
103        let path = path.as_ref();
104
105        if let Some(parent) = path.parent() {
106            std::fs::create_dir_all(parent)?;
107        }
108
109        let data = self.to_json_string()?;
110
111        let mut fh = std::fs::File::create(path)?;
112        let mut permissions = fh.metadata()?.permissions();
113        set_permissions_private(&mut permissions);
114        fh.set_permissions(permissions)?;
115        fh.write_all(data.as_bytes())?;
116
117        Ok(())
118    }
119}
120
121impl TryFrom<UnifiedApiKey> for ConnectTokenEncoder {
122    type Error = anyhow::Error;
123
124    fn try_from(value: UnifiedApiKey) -> Result<Self> {
125        let der = STANDARD_ENGINE
126            .decode(value.private_key)
127            .context("invalid unified api key")?;
128
129        Self::from_ecdsa_der(value.key_id, value.issuer_id, &der)
130    }
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)]
134#[error("invalid PEM formatted private key")]
135pub struct InvalidPemPrivateKey;