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}