Skip to main content

coinbase_advanced/
credentials.rs

1use secrecy::{ExposeSecret, SecretString};
2use std::env;
3
4use crate::error::{Error, Result};
5
6/// Credentials for authenticating with the Coinbase API.
7#[derive(Clone)]
8pub struct Credentials {
9    /// The API key (e.g., "organizations/{org_id}/apiKeys/{key_id}")
10    api_key: String,
11    /// The private key in PEM format (EC P-256)
12    private_key: SecretString,
13}
14
15impl std::fmt::Debug for Credentials {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        f.debug_struct("Credentials")
18            .field("api_key", &self.api_key)
19            .field("private_key", &"[REDACTED]")
20            .finish()
21    }
22}
23
24impl Credentials {
25    /// Create new credentials from an API key and private key.
26    ///
27    /// # Arguments
28    /// * `api_key` - The CDP API key identifier
29    /// * `private_key` - The EC private key in PEM format
30    ///
31    /// # Example
32    /// ```no_run
33    /// use coinbase_advanced::Credentials;
34    ///
35    /// let creds = Credentials::new(
36    ///     "organizations/xxx/apiKeys/yyy",
37    ///     "-----BEGIN EC PRIVATE KEY-----\n...\n-----END EC PRIVATE KEY-----\n"
38    /// ).unwrap();
39    /// ```
40    pub fn new(api_key: impl Into<String>, private_key: impl Into<String>) -> Result<Self> {
41        let api_key = api_key.into();
42        let private_key = private_key.into();
43
44        // Basic validation.
45        if api_key.is_empty() {
46            return Err(Error::config("API key cannot be empty"));
47        }
48        if private_key.is_empty() {
49            return Err(Error::config("Private key cannot be empty"));
50        }
51        if !private_key.contains("BEGIN EC PRIVATE KEY") {
52            return Err(Error::config(
53                "Private key must be in PEM format (EC PRIVATE KEY)",
54            ));
55        }
56
57        Ok(Self {
58            api_key,
59            private_key: SecretString::from(private_key),
60        })
61    }
62
63    /// Create credentials from environment variables.
64    ///
65    /// Reads from:
66    /// - `COINBASE_API_KEY` - The CDP API key
67    /// - `COINBASE_PRIVATE_KEY` - The EC private key in PEM format
68    ///
69    /// Note: The private key should have literal `\n` characters replaced with actual newlines,
70    /// or be stored in a file and read separately.
71    pub fn from_env() -> Result<Self> {
72        let api_key = env::var("COINBASE_API_KEY")
73            .map_err(|_| Error::config("COINBASE_API_KEY environment variable not set"))?;
74
75        let private_key = env::var("COINBASE_PRIVATE_KEY")
76            .map_err(|_| Error::config("COINBASE_PRIVATE_KEY environment variable not set"))?;
77
78        // Handle escaped newlines in environment variable.
79        let private_key = private_key.replace("\\n", "\n");
80
81        Self::new(api_key, private_key)
82    }
83
84    /// Get the API key.
85    pub fn api_key(&self) -> &str {
86        &self.api_key
87    }
88
89    /// Get the private key (exposed for JWT signing).
90    pub(crate) fn private_key(&self) -> &str {
91        self.private_key.expose_secret()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    const TEST_KEY: &str = "organizations/test-org/apiKeys/test-key";
100    const TEST_PRIVATE_KEY: &str = "-----BEGIN EC PRIVATE KEY-----
101MHQCAQEEIBkg4LVWM9nuwNKXPgFvbVwUxYdLlpfazMKfqTgs1RwQoAcGBSuBBAAK
102oUQDQgAEm8+paLliHKY9RI5gZ8SBOHwAFcPf27pePzVTaWLSmzxanOT/MO6DPqMW
1031pNcpaLerRLCPCchK31waXYjKEf3Dw==
104-----END EC PRIVATE KEY-----
105";
106
107    #[test]
108    fn test_new_credentials() {
109        let creds = Credentials::new(TEST_KEY, TEST_PRIVATE_KEY).unwrap();
110        assert_eq!(creds.api_key(), TEST_KEY);
111    }
112
113    #[test]
114    fn test_empty_api_key() {
115        let result = Credentials::new("", TEST_PRIVATE_KEY);
116        assert!(result.is_err());
117    }
118
119    #[test]
120    fn test_empty_private_key() {
121        let result = Credentials::new(TEST_KEY, "");
122        assert!(result.is_err());
123    }
124
125    #[test]
126    fn test_invalid_private_key_format() {
127        let result = Credentials::new(TEST_KEY, "not a pem key");
128        assert!(result.is_err());
129    }
130
131    #[test]
132    fn test_debug_redacts_private_key() {
133        let creds = Credentials::new(TEST_KEY, TEST_PRIVATE_KEY).unwrap();
134        let debug = format!("{:?}", creds);
135        assert!(debug.contains("[REDACTED]"));
136        assert!(!debug.contains("BEGIN EC PRIVATE KEY"));
137    }
138}