1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
use crate::time::sleep;
use std::sync::Arc;
use chrono::{DateTime, Utc};
use rand::random;
use reqwest::IntoUrl;
use sia_core::seed::{self, Seed};
use sia_core::signing::PrivateKey;
use sia_core::types::Hash256;
use thiserror::Error;
use url::Url;
use crate::app_client::{self, Client};
use crate::object_encryption::derive;
use crate::time::Duration;
use crate::{AppID, AppKey, AppMetadata, Sdk};
/// The initial state of the SDK builder, before connecting to the indexd service.
pub struct DisconnectedState;
/// The state of the SDK builder after requesting approval for the application.
pub struct RequestingApprovalState {
response_url: Url,
register_url: Url,
status_url: Url,
expiration: DateTime<Utc>,
}
/// The state of the SDK builder after the application has been approved.
pub struct ApprovedState {
register_url: Url,
user_secret: Hash256,
}
/// A builder for creating an SDK instance.
pub struct Builder<S> {
ephemeral_key: PrivateKey,
state: S,
client: Client,
app_meta: AppMetadata,
}
/// Errors that can occur during the SDK building process.
#[derive(Error, Debug)]
pub enum BuilderError {
/// A URL could not be parsed.
#[error("url error: {0}")]
Url(#[from] url::ParseError),
/// An error from the indexer API client.
#[error("client error: {0}")]
Client(#[from] app_client::Error),
/// A transport-level connection error.
#[error("transport error: {0}")]
Transport(String),
/// The recovery phrase is invalid.
#[error("mnemonic error: {0}")]
Mnemonic(#[from] seed::SeedError),
/// The connection approval request expired before the user approved it.
#[error("request expired")]
RequestExpired,
}
impl Builder<DisconnectedState> {
/// Creates a new SDK builder with the provided indexer URL.
///
/// After creating the builder, call [Builder::connected] to attempt
/// to connect using an existing app key, or [Builder::request_connection]
/// to request a new connection.
///
/// # Example
/// ```rust
/// use sia_storage::{AppMetadata, Builder, app_id};
///
/// const APP_META: AppMetadata = AppMetadata {
/// id: app_id!("a9f0bda1b97b7d44ae6369ac830851a115311bb59aa2d848beda6ae95d10ad18"),
/// name: "My App",
/// description: "My App Description",
/// service_url: "https://myapp.com",
/// logo_url: Some("https://myapp.com/logo.png"),
/// callback_url: Some("https://myapp.com/callback"),
/// };
///
/// let builder = Builder::new("https://sia.storage", APP_META).expect("failed to create builder");
/// ```
pub fn new<U: IntoUrl>(indexer_url: U, app_meta: AppMetadata) -> Result<Self, BuilderError> {
let client = Client::new(indexer_url)?;
Ok(Self {
ephemeral_key: PrivateKey::from_seed(&random::<[u8; 32]>()),
state: DisconnectedState,
client,
app_meta,
})
}
/// Attempts to connect using the provided app key.
/// If the app key is valid, returns Some([Sdk]), otherwise returns None.
///
/// If you receive None, call [Builder::request_connection] to request a new connection.
///
/// # Arguments
/// * `app_key` - The application key used for authentication.
pub async fn connected(&self, app_key: &AppKey) -> Result<Option<Sdk>, BuilderError> {
let connected = self.client.check_app_authenticated(&app_key.0).await?;
if !connected {
return Ok(None);
}
let sdk = Sdk::new(self.client.clone(), Arc::new(app_key.clone())).await?;
Ok(Some(sdk))
}
/// Requests a new connection for the application.
///
/// # Arguments
/// * `app` - Details of the application requesting connection.
pub async fn request_connection(
self,
) -> Result<Builder<RequestingApprovalState>, BuilderError> {
let resp = self
.client
.request_app_connection(&self.ephemeral_key, &self.app_meta)
.await?;
Ok(Builder {
ephemeral_key: self.ephemeral_key,
app_meta: self.app_meta,
state: RequestingApprovalState {
response_url: Url::parse(&resp.response_url)?,
register_url: Url::parse(&resp.register_url)?,
status_url: Url::parse(&resp.status_url)?,
expiration: resp.expiration,
},
client: self.client,
})
}
}
impl Builder<RequestingApprovalState> {
/// Returns the response URL for the registration process. This
/// should be displayed to the user so they can authorize the
/// application.
pub fn response_url(&self) -> &str {
self.state.response_url.as_str()
}
/// Waits for the application registration to be approved. This
/// polls the status URL until the registration is approved or
/// rejected. This can take several minutes depending on user action.
///
/// [Builder::response_url] should be displayed to the user
/// before calling this method.
pub async fn wait_for_approval(self) -> Result<Builder<ApprovedState>, BuilderError> {
loop {
if Utc::now() >= self.state.expiration {
return Err(BuilderError::RequestExpired);
}
if let Some(user_secret) = self
.client
.check_request_status(&self.ephemeral_key, self.state.status_url.clone())
.await?
{
return Ok(Builder {
ephemeral_key: self.ephemeral_key,
state: ApprovedState {
register_url: self.state.register_url.clone(),
user_secret,
},
app_meta: self.app_meta,
client: self.client,
});
}
sleep(Duration::from_secs(5)).await;
}
}
}
impl Builder<ApprovedState> {
/// Completes the registration process and returns an SDK instance.
///
/// # Arguments
/// * `mnemonic` - The user's mnemonic phrase used to derive the application key.
///
/// # Errors
/// Returns [BuilderError] if the registration fails or the SDK cannot be created.
pub async fn register(self, mnemonic: &str) -> Result<Sdk, BuilderError> {
let private_key = derive_app_key(mnemonic, &self.app_meta.id, &self.state.user_secret)?;
self.client
.register_app(
&self.ephemeral_key,
&private_key,
self.state.register_url.clone(),
)
.await?;
Sdk::new(self.client, Arc::new(AppKey(private_key))).await
}
}
/// A helper function to derive an application key from a
/// mnemonic, app ID, and shared secret.
///
/// It is exposed to be able to test the app key derivation logic.
fn derive_app_key(
mnemonic: &str,
app_id: &AppID,
shared_secret: &Hash256,
) -> Result<PrivateKey, BuilderError> {
const KEY_DOMAIN: &[u8] = b"indexd app key derivation";
let seed = Seed::new(mnemonic)?;
let mut key = [0u8; 64];
key[..32].copy_from_slice(seed.entropy());
key[32..].copy_from_slice(shared_secret.as_ref());
let mut okm = [0u8; 32];
derive(&key, app_id.as_ref(), KEY_DOMAIN, &mut okm);
Ok(PrivateKey::from_seed(&okm))
}
#[cfg(test)]
mod test {
use crate::app_id;
use super::*;
use sia_core::hash_256;
use sia_core::types::Hash256;
#[sia_core_derive::cross_target_test]
fn test_app_key_derivation_golden() {
const MNEMONIC: &str =
"glare own entire dish exact open theme family harsh room scrap rose";
const APP_ID: AppID =
app_id!("0e90d697f5045a6593f1c43ebf79a369e2bc72cc5c7b6282f3b5aeb0de6e4005");
const SHARED_SECRET: Hash256 =
hash_256!("cf02d945fe4bfe614d823dc13c19aa8501699e656d0f7915490c3056d5c97dc6");
const EXPECTED_APP_KEY: &str =
"b75061f34bb3aeab232b0671da2d0347c547343a0026bb5535c291d964fd09a1";
let mut seed = [0u8; 32];
hex::decode_to_slice(EXPECTED_APP_KEY, &mut seed).expect("decoding failed");
let expected_app_key = PrivateKey::from_seed(&seed);
let derived_app_key =
derive_app_key(MNEMONIC, &APP_ID, &SHARED_SECRET).expect("derivation failed");
assert_eq!(derived_app_key, expected_app_key);
}
}