1 use std::{env, fmt};
2 
3 use serde::{de::DeserializeOwned, Serialize};
4 
5 use rpki::uri;
6 
7 use crate::{
8     cli::{
9         options::{BulkCaCommand, CaCommand, Command, KrillInitDetails, Options, PubServerCommand},
10         report::{ApiResponse, ReportError},
11     },
12     commons::{
13         api::{
14             AllCertAuthIssues, AspaDefinitionUpdates, CaRepoDetails, CertAuthIssues, ChildCaInfo,
15             ChildrenConnectionStats, ParentCaContact, ParentStatuses, PublisherDetails, PublisherList, RepoStatus,
16             Token,
17         },
18         bgp::BgpAnalysisAdvice,
19         error::KrillIoError,
20         remote::rfc8183,
21         util::{file, httpclient},
22     },
23     constants::KRILL_CLI_API_ENV,
24     daemon::config::Config,
25 };
26 
27 #[cfg(feature = "multi-user")]
28 use crate::{
29     cli::options::KrillUserDetails,
30     constants::{PW_HASH_LOG_N, PW_HASH_P, PW_HASH_R},
31 };
32 
resolve_uri(server: &uri::Https, path: &str) -> String33 fn resolve_uri(server: &uri::Https, path: &str) -> String {
34     format!("{}{}", server, path)
35 }
36 
get_json<T: DeserializeOwned>(server: &uri::Https, token: &Token, path: &str) -> Result<T, Error>37 async fn get_json<T: DeserializeOwned>(server: &uri::Https, token: &Token, path: &str) -> Result<T, Error> {
38     let uri = resolve_uri(server, path);
39     httpclient::get_json(&uri, Some(token))
40         .await
41         .map_err(Error::HttpClientError)
42 }
43 
post_empty(server: &uri::Https, token: &Token, path: &str) -> Result<(), Error>44 async fn post_empty(server: &uri::Https, token: &Token, path: &str) -> Result<(), Error> {
45     let uri = resolve_uri(server, path);
46     httpclient::post_empty(&uri, Some(token))
47         .await
48         .map_err(Error::HttpClientError)
49 }
50 
post_json(server: &uri::Https, token: &Token, path: &str, data: impl Serialize) -> Result<(), Error>51 async fn post_json(server: &uri::Https, token: &Token, path: &str, data: impl Serialize) -> Result<(), Error> {
52     let uri = resolve_uri(server, path);
53     httpclient::post_json(&uri, data, Some(token))
54         .await
55         .map_err(Error::HttpClientError)
56 }
57 
post_json_with_response<T: DeserializeOwned>( server: &uri::Https, token: &Token, path: &str, data: impl Serialize, ) -> Result<T, Error>58 async fn post_json_with_response<T: DeserializeOwned>(
59     server: &uri::Https,
60     token: &Token,
61     path: &str,
62     data: impl Serialize,
63 ) -> Result<T, Error> {
64     let uri = resolve_uri(server, path);
65     httpclient::post_json_with_response(&uri, data, Some(token))
66         .await
67         .map_err(Error::HttpClientError)
68 }
69 
post_json_with_opt_response<T: DeserializeOwned>( server: &uri::Https, token: &Token, uri: &str, data: impl Serialize, ) -> Result<Option<T>, Error>70 async fn post_json_with_opt_response<T: DeserializeOwned>(
71     server: &uri::Https,
72     token: &Token,
73     uri: &str,
74     data: impl Serialize,
75 ) -> Result<Option<T>, Error> {
76     let uri = resolve_uri(server, uri);
77     httpclient::post_json_with_opt_response(&uri, data, Some(token))
78         .await
79         .map_err(Error::HttpClientError)
80 }
81 
delete(server: &uri::Https, token: &Token, uri: &str) -> Result<(), Error>82 async fn delete(server: &uri::Https, token: &Token, uri: &str) -> Result<(), Error> {
83     let uri = resolve_uri(server, uri);
84     httpclient::delete(&uri, Some(token))
85         .await
86         .map_err(Error::HttpClientError)
87 }
88 
89 /// Command line tool for Krill admin tasks
90 pub struct KrillClient {
91     server: uri::Https,
92     token: Token,
93 }
94 
95 impl KrillClient {
96     /// Delegates the options to be processed, and reports the response
97     /// back to the user. Note that error reporting is handled by CLI.
report(options: Options) -> Result<(), Error>98     pub async fn report(options: Options) -> Result<(), Error> {
99         let format = options.format;
100         let res = Self::process(options).await?;
101 
102         if let Some(string) = res.report(format)? {
103             println!("{}", string)
104         }
105         Ok(())
106     }
107 
108     /// Processes the options, and returns a response ready for formatting.
109     /// Note that this function is public to help integration testing the API
110     /// and client.
process(options: Options) -> Result<ApiResponse, Error>111     pub async fn process(options: Options) -> Result<ApiResponse, Error> {
112         let client = KrillClient {
113             server: options.server,
114             token: options.token,
115         };
116 
117         if options.api {
118             // passing the api option in the env, so that the call
119             // to the back-end will just print and exit.
120             env::set_var(KRILL_CLI_API_ENV, "1")
121         }
122 
123         trace!("Sending command: {:?}", options.command);
124 
125         match options.command {
126             Command::Health => client.health().await,
127             Command::Info => client.info().await,
128             Command::Bulk(cmd) => client.bulk(cmd).await,
129             Command::CertAuth(cmd) => client.certauth(cmd).await,
130             Command::PubServer(cmd) => client.publishers(cmd).await,
131             Command::Init(details) => client.init_config(details),
132             #[cfg(feature = "multi-user")]
133             Command::User(cmd) => client.user(cmd),
134             Command::NotSet => Err(Error::MissingCommand),
135         }
136     }
137 
health(&self) -> Result<ApiResponse, Error>138     async fn health(&self) -> Result<ApiResponse, Error> {
139         httpclient::get_ok(&resolve_uri(&self.server, "api/v1/authorized"), Some(&self.token)).await?;
140         Ok(ApiResponse::Health)
141     }
142 
info(&self) -> Result<ApiResponse, Error>143     async fn info(&self) -> Result<ApiResponse, Error> {
144         let info = httpclient::get_json(&resolve_uri(&self.server, "stats/info"), Some(&self.token)).await?;
145         Ok(ApiResponse::Info(info))
146     }
147 
bulk(&self, command: BulkCaCommand) -> Result<ApiResponse, Error>148     async fn bulk(&self, command: BulkCaCommand) -> Result<ApiResponse, Error> {
149         match command {
150             BulkCaCommand::Refresh => {
151                 post_empty(&self.server, &self.token, "api/v1/bulk/cas/sync/parent").await?;
152                 Ok(ApiResponse::Empty)
153             }
154             BulkCaCommand::Publish => {
155                 post_empty(&self.server, &self.token, "api/v1/bulk/cas/publish").await?;
156                 Ok(ApiResponse::Empty)
157             }
158             BulkCaCommand::Sync => {
159                 post_empty(&self.server, &self.token, "api/v1/bulk/cas/sync/repo").await?;
160                 Ok(ApiResponse::Empty)
161             }
162         }
163     }
164 
165     #[allow(clippy::cognitive_complexity)]
certauth(&self, command: CaCommand) -> Result<ApiResponse, Error>166     async fn certauth(&self, command: CaCommand) -> Result<ApiResponse, Error> {
167         match command {
168             CaCommand::Init(init) => {
169                 post_json(&self.server, &self.token, "api/v1/cas", init).await?;
170                 Ok(ApiResponse::Empty)
171             }
172 
173             CaCommand::Delete(ca) => {
174                 let uri = format!("api/v1/cas/{}", ca);
175                 delete(&self.server, &self.token, &uri).await?;
176                 Ok(ApiResponse::Empty)
177             }
178 
179             CaCommand::UpdateId(handle) => {
180                 let uri = format!("api/v1/cas/{}/id", handle);
181                 post_empty(&self.server, &self.token, &uri).await?;
182                 Ok(ApiResponse::Empty)
183             }
184 
185             CaCommand::ParentResponse(handle, child) => {
186                 let uri = format!("api/v1/cas/{}/children/{}/contact", handle, child);
187                 let info: ParentCaContact = get_json(&self.server, &self.token, &uri).await?;
188                 Ok(ApiResponse::ParentCaContact(info))
189             }
190 
191             CaCommand::ChildRequest(handle) => {
192                 let uri = format!("api/v1/cas/{}/id/child_request.json", handle);
193                 let req = get_json(&self.server, &self.token, &uri).await?;
194                 Ok(ApiResponse::Rfc8183ChildRequest(req))
195             }
196 
197             CaCommand::RepoPublisherRequest(handle) => {
198                 let uri = format!("api/v1/cas/{}/id/publisher_request.json", handle);
199                 let req: rfc8183::PublisherRequest = get_json(&self.server, &self.token, &uri).await?;
200                 Ok(ApiResponse::Rfc8183PublisherRequest(req))
201             }
202 
203             CaCommand::RepoDetails(handle) => {
204                 let uri = format!("api/v1/cas/{}/repo", handle);
205                 let details: CaRepoDetails = get_json(&self.server, &self.token, &uri).await?;
206                 Ok(ApiResponse::RepoDetails(details))
207             }
208 
209             CaCommand::RepoStatus(ca) => {
210                 let uri = format!("api/v1/cas/{}/repo/status", ca);
211                 let status: RepoStatus = get_json(&self.server, &self.token, &uri).await?;
212                 Ok(ApiResponse::RepoStatus(status))
213             }
214 
215             CaCommand::RepoUpdate(handle, update) => {
216                 let uri = format!("api/v1/cas/{}/repo", handle);
217                 post_json(&self.server, &self.token, &uri, update).await?;
218                 Ok(ApiResponse::Empty)
219             }
220 
221             CaCommand::AddParent(handle, parent_req) => {
222                 let uri = format!("api/v1/cas/{}/parents", handle);
223                 post_json(&self.server, &self.token, &uri, parent_req).await?;
224                 Ok(ApiResponse::Empty)
225             }
226 
227             CaCommand::RemoveParent(handle, parent) => {
228                 let uri = format!("api/v1/cas/{}/parents/{}", handle, parent);
229                 delete(&self.server, &self.token, &uri).await?;
230                 Ok(ApiResponse::Empty)
231             }
232 
233             CaCommand::ParentStatuses(handle) => {
234                 let uri = format!("api/v1/cas/{}/parents", handle);
235                 let statuses: ParentStatuses = get_json(&self.server, &self.token, &uri).await?;
236                 Ok(ApiResponse::ParentStatuses(statuses))
237             }
238 
239             CaCommand::MyParentCaContact(handle, parent) => {
240                 let uri = format!("api/v1/cas/{}/parents/{}", handle, parent);
241                 let parent: ParentCaContact = get_json(&self.server, &self.token, &uri).await?;
242                 Ok(ApiResponse::ParentCaContact(parent))
243             }
244 
245             CaCommand::Refresh(handle) => {
246                 let uri = format!("api/v1/cas/{}/sync/parents", handle);
247                 post_empty(&self.server, &self.token, &uri).await?;
248                 Ok(ApiResponse::Empty)
249             }
250 
251             CaCommand::ChildInfo(handle, child) => {
252                 let uri = format!("api/v1/cas/{}/children/{}", handle, child);
253                 let info: ChildCaInfo = get_json(&self.server, &self.token, &uri).await?;
254                 Ok(ApiResponse::ChildInfo(info))
255             }
256 
257             CaCommand::ChildAdd(handle, req) => {
258                 let uri = format!("api/v1/cas/{}/children", handle);
259                 let info: ParentCaContact = post_json_with_response(&self.server, &self.token, &uri, req).await?;
260                 Ok(ApiResponse::ParentCaContact(info))
261             }
262             CaCommand::ChildUpdate(handle, child, req) => {
263                 let uri = format!("api/v1/cas/{}/children/{}", handle, child);
264                 post_json(&self.server, &self.token, &uri, req).await?;
265                 Ok(ApiResponse::Empty)
266             }
267             CaCommand::ChildDelete(handle, child) => {
268                 let uri = format!("api/v1/cas/{}/children/{}", handle, child);
269                 delete(&self.server, &self.token, &uri).await?;
270                 Ok(ApiResponse::Empty)
271             }
272             CaCommand::ChildConnections(handle) => {
273                 let uri = format!("api/v1/cas/{}/stats/children/connections", handle);
274                 let stats: ChildrenConnectionStats = get_json(&self.server, &self.token, &uri).await?;
275                 Ok(ApiResponse::ChildrenStats(stats))
276             }
277 
278             CaCommand::KeyRollInit(handle) => {
279                 let uri = format!("api/v1/cas/{}/keys/roll_init", handle);
280                 post_empty(&self.server, &self.token, &uri).await?;
281                 Ok(ApiResponse::Empty)
282             }
283             CaCommand::KeyRollActivate(handle) => {
284                 let uri = format!("api/v1/cas/{}/keys/roll_activate", handle);
285                 post_empty(&self.server, &self.token, &uri).await?;
286                 Ok(ApiResponse::Empty)
287             }
288 
289             CaCommand::RouteAuthorizationsList(handle) => {
290                 let uri = format!("api/v1/cas/{}/routes", handle);
291                 let roas = get_json(&self.server, &self.token, &uri).await?;
292                 Ok(ApiResponse::RouteAuthorizations(roas))
293             }
294 
295             CaCommand::RouteAuthorizationsUpdate(handle, updates) => {
296                 let uri = format!("api/v1/cas/{}/routes", handle);
297                 post_json(&self.server, &self.token, &uri, updates).await?;
298                 Ok(ApiResponse::Empty)
299             }
300 
301             CaCommand::RouteAuthorizationsTryUpdate(handle, updates) => {
302                 let uri = format!("api/v1/cas/{}/routes/try", handle);
303                 let advice_opt: Option<BgpAnalysisAdvice> =
304                     post_json_with_opt_response(&self.server, &self.token, &uri, updates).await?;
305                 match advice_opt {
306                     None => Ok(ApiResponse::Empty),
307                     Some(advice) => Ok(ApiResponse::BgpAnalysisAdvice(advice)),
308                 }
309             }
310 
311             CaCommand::RouteAuthorizationsDryRunUpdate(handle, updates) => {
312                 let uri = format!("api/v1/cas/{}/routes/analysis/dryrun", handle);
313                 let report = post_json_with_response(&self.server, &self.token, &uri, updates).await?;
314                 Ok(ApiResponse::BgpAnalysisFull(report))
315             }
316 
317             CaCommand::BgpAnalysisFull(handle) => {
318                 let uri = format!("api/v1/cas/{}/routes/analysis/full", handle);
319                 let report = get_json(&self.server, &self.token, &uri).await?;
320                 Ok(ApiResponse::BgpAnalysisFull(report))
321             }
322 
323             CaCommand::BgpAnalysisSuggest(handle, resources) => {
324                 let uri = format!("api/v1/cas/{}/routes/analysis/suggest", handle);
325 
326                 let suggestions = if let Some(resources) = resources {
327                     post_json_with_response(&self.server, &self.token, &uri, resources).await?
328                 } else {
329                     get_json(&self.server, &self.token, &uri).await?
330                 };
331 
332                 Ok(ApiResponse::BgpAnalysisSuggestions(suggestions))
333             }
334 
335             CaCommand::AspasList(handle) => {
336                 let uri = format!("api/v1/cas/{}/aspas", handle);
337                 let aspas = get_json(&self.server, &self.token, &uri).await?;
338                 Ok(ApiResponse::AspaDefinitions(aspas))
339             }
340 
341             CaCommand::AspasAddOrReplace(handle, aspa) => {
342                 let uri = format!("api/v1/cas/{}/aspas", handle);
343                 let updates = AspaDefinitionUpdates::new(vec![aspa], vec![]);
344                 post_json(&self.server, &self.token, &uri, updates).await?;
345                 Ok(ApiResponse::Empty)
346             }
347 
348             CaCommand::AspasRemove(handle, customer) => {
349                 let uri = format!("api/v1/cas/{}/aspas", handle);
350                 let updates = AspaDefinitionUpdates::new(vec![], vec![customer]);
351                 post_json(&self.server, &self.token, &uri, updates).await?;
352                 Ok(ApiResponse::Empty)
353             }
354 
355             CaCommand::AspasUpdate(handle, customer, update) => {
356                 let uri = format!("api/v1/cas/{}/aspas/as/{}", handle, customer);
357                 post_json(&self.server, &self.token, &uri, update).await?;
358                 Ok(ApiResponse::Empty)
359             }
360 
361             CaCommand::Show(handle) => {
362                 let uri = format!("api/v1/cas/{}", handle);
363                 let ca_info = get_json(&self.server, &self.token, &uri).await?;
364 
365                 Ok(ApiResponse::CertAuthInfo(ca_info))
366             }
367 
368             CaCommand::ShowHistoryCommands(handle, options) => {
369                 let uri = format!(
370                     "api/v1/cas/{}/history/commands/{}",
371                     handle,
372                     options.url_path_parameters()
373                 );
374                 let history = get_json(&self.server, &self.token, &uri).await?;
375 
376                 Ok(ApiResponse::CertAuthHistory(history))
377             }
378 
379             CaCommand::ShowHistoryDetails(handle, key) => {
380                 let uri = format!("api/v1/cas/{}/history/details/{}", handle, key);
381                 let action = get_json(&self.server, &self.token, &uri).await?;
382 
383                 Ok(ApiResponse::CertAuthAction(action))
384             }
385 
386             CaCommand::Issues(ca_opt) => match ca_opt {
387                 Some(ca) => {
388                     let uri = format!("api/v1/cas/{}/issues", ca);
389                     let issues: CertAuthIssues = get_json(&self.server, &self.token, &uri).await?;
390                     Ok(ApiResponse::CertAuthIssues(issues))
391                 }
392                 None => {
393                     let issues: AllCertAuthIssues =
394                         get_json(&self.server, &self.token, "api/v1/bulk/cas/issues").await?;
395                     Ok(ApiResponse::AllCertAuthIssues(issues))
396                 }
397             },
398 
399             CaCommand::RtaList(ca) => {
400                 let uri = format!("api/v1/cas/{}/rta/", ca);
401                 let list = get_json(&self.server, &self.token, &uri).await?;
402                 Ok(ApiResponse::RtaList(list))
403             }
404 
405             CaCommand::RtaShow(ca, name, out) => {
406                 let uri = format!("api/v1/cas/{}/rta/{}", ca, name);
407                 let rta = get_json(&self.server, &self.token, &uri).await?;
408 
409                 match out {
410                     None => Ok(ApiResponse::Rta(rta)),
411                     Some(out) => {
412                         file::save(rta.as_ref(), &out)?;
413                         Ok(ApiResponse::Empty)
414                     }
415                 }
416             }
417 
418             CaCommand::RtaSign(ca, name, request) => {
419                 let uri = format!("api/v1/cas/{}/rta/{}/sign", ca, name);
420                 post_json(&self.server, &self.token, &uri, request).await?;
421                 Ok(ApiResponse::Empty)
422             }
423 
424             CaCommand::RtaMultiPrep(ca, name, resources) => {
425                 let uri = format!("api/v1/cas/{}/rta/{}/multi/prep", ca, name);
426                 let response = post_json_with_response(&self.server, &self.token, &uri, resources).await?;
427                 Ok(ApiResponse::RtaMultiPrep(response))
428             }
429 
430             CaCommand::RtaMultiCoSign(ca, name, rta) => {
431                 let uri = format!("api/v1/cas/{}/rta/{}/multi/cosign", ca, name);
432                 post_json(&self.server, &self.token, &uri, rta).await?;
433                 Ok(ApiResponse::Empty)
434             }
435 
436             CaCommand::List => {
437                 let cas = get_json(&self.server, &self.token, "api/v1/cas").await?;
438                 Ok(ApiResponse::CertAuths(cas))
439             }
440         }
441     }
442 
443     /// Processes the options, and returns a response ready for formatting.
444     /// Note that this function is public to help integration testing the API
445     /// and client.
publishers(&self, command: PubServerCommand) -> Result<ApiResponse, Error>446     pub async fn publishers(&self, command: PubServerCommand) -> Result<ApiResponse, Error> {
447         match command {
448             PubServerCommand::PublisherList => {
449                 let list: PublisherList = get_json(&self.server, &self.token, "api/v1/pubd/publishers").await?;
450                 Ok(ApiResponse::PublisherList(list))
451             }
452             PubServerCommand::StalePublishers(seconds) => {
453                 let uri = format!("api/v1/pubd/stale/{}", seconds);
454                 let stales = get_json(&self.server, &self.token, &uri).await?;
455                 Ok(ApiResponse::PublisherList(stales))
456             }
457             PubServerCommand::RepositoryStats => {
458                 let stats = get_json(&self.server, &self.token, "stats/repo").await?;
459                 Ok(ApiResponse::RepoStats(stats))
460             }
461             PubServerCommand::RepositoryInit(uris) => {
462                 let uri = "api/v1/pubd/init";
463                 post_json(&self.server, &self.token, uri, uris).await?;
464                 Ok(ApiResponse::Empty)
465             }
466             PubServerCommand::RepositoryClear => {
467                 let uri = "api/v1/pubd/init";
468                 delete(&self.server, &self.token, uri).await?;
469                 Ok(ApiResponse::Empty)
470             }
471             PubServerCommand::AddPublisher(req) => {
472                 let res = post_json_with_response(&self.server, &self.token, "api/v1/pubd/publishers", req).await?;
473                 Ok(ApiResponse::Rfc8183RepositoryResponse(res))
474             }
475             PubServerCommand::RemovePublisher(handle) => {
476                 let uri = format!("api/v1/pubd/publishers/{}", handle);
477                 delete(&self.server, &self.token, &uri).await?;
478                 Ok(ApiResponse::Empty)
479             }
480             PubServerCommand::ShowPublisher(handle) => {
481                 let uri = format!("api/v1/pubd/publishers/{}", handle);
482                 let details: PublisherDetails = get_json(&self.server, &self.token, &uri).await?;
483                 Ok(ApiResponse::PublisherDetails(details))
484             }
485             PubServerCommand::RepositoryResponse(handle) => {
486                 let uri = format!("api/v1/pubd/publishers/{}/response.json", handle);
487                 let res = get_json(&self.server, &self.token, &uri).await?;
488                 Ok(ApiResponse::Rfc8183RepositoryResponse(res))
489             }
490         }
491     }
492 
init_config(&self, details: KrillInitDetails) -> Result<ApiResponse, Error>493     fn init_config(&self, details: KrillInitDetails) -> Result<ApiResponse, Error> {
494         let defaults = include_str!("../../defaults/krill.conf");
495         let multi_add_on = include_str!("../../defaults/krill-multi-user.conf");
496 
497         let mut config = defaults.to_string();
498         config = config.replace("### admin_token =", &format!("admin_token = \"{}\"", self.token));
499 
500         config = config.replace(
501             "### service_uri = \"https://localhost:3000/\"",
502             &format!("service_uri = \"{}\"", self.server),
503         );
504 
505         if let Some(data_dir) = details.data_dir() {
506             config = config.replace("### data_dir = \"./data\"", &format!("data_dir = \"{}\"", data_dir))
507         }
508 
509         if let Some(log_file) = details.log_file() {
510             config = config.replace(
511                 "### log_file = \"./krill.log\"",
512                 &format!("log_file = \"{}\"", log_file),
513             )
514         }
515 
516         if details.multi_user() {
517             config.push_str("\n\n\n");
518             config.push_str(multi_add_on);
519         }
520 
521         let c: Config = toml::from_slice(config.as_ref()).map_err(Error::init)?;
522         c.verify().map_err(Error::init)?;
523 
524         Ok(ApiResponse::GenericBody(config))
525     }
526 
527     #[cfg(feature = "multi-user")]
528     #[allow(clippy::unnecessary_wraps)]
user(&self, details: KrillUserDetails) -> Result<ApiResponse, Error>529     fn user(&self, details: KrillUserDetails) -> Result<ApiResponse, Error> {
530         let (password_hash, salt) = {
531             use scrypt::scrypt;
532 
533             let password = rpassword::read_password_from_tty(Some("Enter the password to hash: ")).unwrap();
534 
535             // The scrypt-js NPM documentation (https://www.npmjs.com/package/scrypt-js) says:
536             //   "TL;DR - either only allow ASCII characters in passwords, or use
537             //            String.prototype.normalize('NFKC') on any password"
538             // So in Lagosta we do the NFKC normalization and thus we need to do the same here.
539             use unicode_normalization::UnicodeNormalization;
540 
541             let user_id = details.id().nfkc().collect::<String>();
542             let password = password.trim().nfkc().collect::<String>();
543             let params = scrypt::Params::new(PW_HASH_LOG_N, PW_HASH_R, PW_HASH_P).unwrap();
544 
545             // hash twice with two different salts
546             // hash first with a salt the client browser knows how to construct based on the users id and a site
547             // specific string.
548 
549             let weak_salt = format!("krill-lagosta-{}", user_id);
550             let weak_salt = weak_salt.nfkc().collect::<String>();
551 
552             let mut interim_hash: [u8; 32] = [0; 32];
553             scrypt(password.as_bytes(), weak_salt.as_bytes(), &params, &mut interim_hash).unwrap();
554 
555             // hash again using a strong random salt only known to the server
556             let mut strong_salt: [u8; 32] = [0; 32];
557             openssl::rand::rand_bytes(&mut strong_salt).unwrap();
558             let mut final_hash: [u8; 32] = [0; 32];
559             scrypt(&interim_hash, &strong_salt, &params, &mut final_hash).unwrap();
560 
561             (final_hash, strong_salt)
562         };
563 
564         // Due to https://github.com/alexcrichton/toml-rs/issues/406 we cannot
565         // produce inline table style TOML by serializing from config structs to
566         // a string using the toml crate. Instead we build it up ourselves.
567         let attrs = details.attrs();
568         let attrs_fragment = match attrs.is_empty() {
569             false => format!(
570                 "attributes={{ {} }}, ",
571                 attrs
572                     .iter()
573                     // quote the key if needed
574                     .map(|(k, v)| match k.contains(' ') {
575                         true => (format!(r#""{}""#, k), v),
576                         false => (k.clone(), v),
577                     })
578                     // quote the value
579                     .map(|(k, v)| format!(r#"{}="{}""#, k, v))
580                     .collect::<Vec<String>>()
581                     .join(", ")
582             ),
583             true => String::new(),
584         };
585 
586         let toml = format!(
587             r#"
588 [auth_users]
589 "{id}" = {{ {attrs}password_hash="{ph}", salt="{salt}" }}"#,
590             id = details.id(),
591             attrs = attrs_fragment,
592             ph = hex::encode(password_hash),
593             salt = hex::encode(salt),
594         );
595 
596         Ok(ApiResponse::GenericBody(toml))
597     }
598 }
599 
600 //------------ Error ---------------------------------------------------------
601 
602 #[derive(Debug)]
603 #[allow(clippy::large_enum_variant)]
604 pub enum Error {
605     MissingCommand,
606     ServerDown,
607     HttpClientError(httpclient::Error),
608     ReportError(ReportError),
609     IoError(KrillIoError),
610     EmptyResponse,
611     Rfc8183(rfc8183::Error),
612     InitError(String),
613     InputError(String),
614 }
615 
616 impl fmt::Display for Error {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result617     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
618         match self {
619             Error::MissingCommand => write!(f, "No valid command given, see --help"),
620             Error::ServerDown => write!(f, "Server is not available."),
621             Error::HttpClientError(e) => write!(f, "Http client error: {}", e),
622             Error::ReportError(e) => e.fmt(f),
623             Error::IoError(e) => write!(f, "I/O error: {}", e),
624             Error::EmptyResponse => write!(f, "Empty response received from server"),
625             Error::Rfc8183(e) => e.fmt(f),
626             Error::InitError(s) => s.fmt(f),
627             Error::InputError(s) => s.fmt(f),
628         }
629     }
630 }
631 
632 impl Error {
init(msg: impl fmt::Display) -> Self633     fn init(msg: impl fmt::Display) -> Self {
634         Error::InitError(msg.to_string())
635     }
636 }
637 
638 impl From<httpclient::Error> for Error {
from(e: httpclient::Error) -> Self639     fn from(e: httpclient::Error) -> Self {
640         Error::HttpClientError(e)
641     }
642 }
643 
644 impl From<KrillIoError> for Error {
from(e: KrillIoError) -> Self645     fn from(e: KrillIoError) -> Self {
646         Error::IoError(e)
647     }
648 }
649 
650 impl From<ReportError> for Error {
from(e: ReportError) -> Self651     fn from(e: ReportError) -> Self {
652         Error::ReportError(e)
653     }
654 }
655 
656 impl From<rfc8183::Error> for Error {
from(e: rfc8183::Error) -> Error657     fn from(e: rfc8183::Error) -> Error {
658         Error::Rfc8183(e)
659     }
660 }
661 
662 //------------ Tests ---------------------------------------------------------
663 
664 #[cfg(test)]
665 mod tests {
666 
667     use super::*;
668     use crate::cli::options::KrillInitDetails;
669     use crate::test;
670 
671     #[test]
init_config_file()672     fn init_config_file() {
673         let mut details = KrillInitDetails::default();
674         details.with_data_dir("/var/lib/krill/data/");
675         details.with_log_file("/var/log/krill/krill.log");
676 
677         let client = KrillClient {
678             server: test::https("https://localhost:3001/"),
679             token: Token::from("secret"),
680         };
681 
682         let res = client.init_config(details).unwrap();
683 
684         match res {
685             ApiResponse::GenericBody(body) => {
686                 let expected = include_str!("../../test-resources/krill-init.conf");
687                 assert_eq!(expected, &body)
688             }
689             _ => panic!("Expected body"),
690         }
691     }
692 
693     #[test]
init_multi_user_config_file()694     fn init_multi_user_config_file() {
695         let mut details = KrillInitDetails::multi_user_dflt();
696         details.with_data_dir("/var/lib/krill/data/");
697         details.with_log_file("/var/log/krill/krill.log");
698 
699         let client = KrillClient {
700             server: test::https("https://localhost:3001/"),
701             token: Token::from("secret"),
702         };
703 
704         let res = client.init_config(details).unwrap();
705 
706         match res {
707             ApiResponse::GenericBody(body) => {
708                 let expected = include_str!("../../test-resources/krill-init-multi-user.conf");
709                 assert_eq!(expected, &body)
710             }
711             _ => panic!("Expected body"),
712         }
713     }
714 }
715