nautilus_kraken/common/
credential.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Request signing and authentication credentials for the Kraken API.
17
18use std::collections::HashMap;
19
20use aws_lc_rs::{digest, hmac};
21use base64::{Engine, engine::general_purpose::STANDARD};
22use serde_urlencoded;
23use zeroize::{Zeroize, ZeroizeOnDrop};
24
25#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
26pub struct KrakenCredential {
27    api_key: String,
28    api_secret: String,
29}
30
31impl KrakenCredential {
32    pub fn new(api_key: impl Into<String>, api_secret: impl Into<String>) -> Self {
33        Self {
34            api_key: api_key.into(),
35            api_secret: api_secret.into(),
36        }
37    }
38
39    pub fn api_key(&self) -> &str {
40        &self.api_key
41    }
42
43    /// Sign a request for Kraken REST API.
44    ///
45    /// Kraken uses HMAC-SHA512 with the following message:
46    /// - path + SHA256(nonce + POST data)
47    /// - The secret is base64 decoded before signing
48    pub fn sign_request(
49        &self,
50        path: &str,
51        nonce: u64,
52        params: &HashMap<String, String>,
53    ) -> anyhow::Result<(String, String)> {
54        // Decode the secret from base64
55        let secret = STANDARD
56            .decode(&self.api_secret)
57            .map_err(|e| anyhow::anyhow!("Failed to decode API secret: {e}"))?;
58
59        // Create POST data string
60        let mut post_data = format!("nonce={nonce}");
61        if !params.is_empty() {
62            let encoded = serde_urlencoded::to_string(params)
63                .map_err(|e| anyhow::anyhow!("Failed to encode params: {e}"))?;
64            post_data.push('&');
65            post_data.push_str(&encoded);
66        }
67
68        // Hash the nonce + POST data with SHA256
69        let hash = digest::digest(&digest::SHA256, post_data.as_bytes());
70
71        // Concatenate path + hash
72        let mut message = path.as_bytes().to_vec();
73        message.extend_from_slice(hash.as_ref());
74
75        // Sign with HMAC-SHA512
76        let key = hmac::Key::new(hmac::HMAC_SHA512, &secret);
77        let signature = hmac::sign(&key, &message);
78
79        // Encode signature as base64 and return with post_data
80        Ok((STANDARD.encode(signature.as_ref()), post_data))
81    }
82
83    /// Returns a masked version of the API key for logging purposes.
84    ///
85    /// Shows first 4 and last 4 characters with ellipsis in between.
86    /// For keys shorter than 8 characters, shows asterisks only.
87    #[must_use]
88    pub fn api_key_masked(&self) -> String {
89        nautilus_core::string::mask_api_key(&self.api_key)
90    }
91}
92
93////////////////////////////////////////////////////////////////////////////////
94// Tests
95////////////////////////////////////////////////////////////////////////////////
96
97#[cfg(test)]
98mod tests {
99    use rstest::rstest;
100
101    use super::*;
102
103    #[rstest]
104    fn test_credential_creation() {
105        let cred = KrakenCredential::new("test_key", "test_secret");
106        assert_eq!(cred.api_key(), "test_key");
107    }
108}