keeper_secrets_manager_core/utils.rs
1// -*- coding: utf-8 -*-
2// _ __
3// | |/ /___ ___ _ __ ___ _ _ (R)
4// | ' </ -_) -_) '_ \/ -_) '_|
5// |_|\_\___\___| .__/\___|_|
6// |_|
7//
8// Keeper Secrets Manager
9// Copyright 2024 Keeper Security Inc.
10// Contact: sm@keepersecurity.com
11//
12
13use crate::{
14 crypto::{unpad_data, CryptoUtils},
15 custom_error::KSMRError,
16};
17use base64::{
18 engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
19 prelude::BASE64_URL_SAFE,
20 Engine as _,
21};
22use chrono::Utc;
23use core::str;
24#[cfg(feature = "totp")]
25use data_encoding::BASE32;
26#[cfg(feature = "totp")]
27use hmac::{Hmac, Mac};
28use log::warn;
29use num_bigint::BigUint;
30#[cfg(feature = "password-gen")]
31use rand::seq::SliceRandom;
32use rand::{seq::IteratorRandom, thread_rng};
33use serde::Serialize;
34use serde_json::Value;
35#[cfg(feature = "totp")]
36use sha1::Sha1;
37#[cfg(feature = "totp")]
38use sha2::{Sha256, Sha512};
39use std::process::Output;
40use std::{collections::HashMap, option::Option};
41use std::{env, io};
42#[cfg(feature = "totp")]
43use url::{form_urlencoded::parse, Url};
44
45#[cfg(unix)]
46use std::os::unix::fs::PermissionsExt;
47
48#[cfg(target_os = "windows")]
49use log::debug;
50use std::fs::File;
51#[cfg(target_os = "windows")]
52use std::process::Command;
53
54#[cfg(unix)]
55use std::fs;
56
57/// Allowed Windows configuration administrators.
58pub const ALLOWED_WINDOWS_CONFIG_ADMINS: [&[u8]; 2] = [b"Administrators", b"SYSTEM"];
59
60/// Encoding format.
61pub const ENCODING: &str = "UTF-8";
62
63/// Special characters used in passwords or other contexts.
64pub const SPECIAL_CHARACTERS: &str = r#"""!@#$%()+;<>=?[]{}^.,"""#;
65
66/// Default password length.
67pub const DEFAULT_PASSWORD_LENGTH: usize = 32;
68
69/// Converts a string representation of truth to a boolean value.
70///
71/// The function accepts string values that represent true or false:
72/// - True values: "y", "yes", "t", "true", "on", "1"
73/// - False values: "n", "no", "f", "false", "off", "0"
74///
75/// # Errors
76///
77/// Returns an error if the input string does not match any of the valid
78/// truth values.
79///
80/// # Examples
81///
82/// ```
83/// use keeper_secrets_manager_core::utils::str_to_bool;
84/// let true_value = str_to_bool("yes").unwrap();
85/// assert_eq!(true_value, true);
86///
87/// let false_value = str_to_bool("no").unwrap();
88/// assert_eq!(false_value, false);
89///
90/// let invalid_value = str_to_bool("maybe");
91/// assert!(invalid_value.is_err());
92/// ```
93pub fn str_to_bool(val: &str) -> Result<bool, String> {
94 let val = val.to_lowercase();
95 match val.as_str() {
96 "y" | "yes" | "t" | "true" | "on" | "1" => Ok(true),
97 "n" | "no" | "f" | "false" | "off" | "0" => Ok(false),
98 _ => Err(format!("invalid truth value {:?}", val)),
99 }
100}
101
102/// Gets the name of the operating system.
103///
104/// This function returns a string slice that indicates the current
105/// operating system. The possible return values are:
106///
107/// - `"linux"` for Linux operating systems.
108/// - `"macOS"` for macOS.
109/// - The value of `std::env::consts::OS` for any other operating systems.
110///
111/// # Examples
112///
113/// ```
114/// use keeper_secrets_manager_core::utils::get_os;
115/// let os_name = get_os();
116/// println!("Operating System: {}", os_name);
117/// ```
118pub fn get_os() -> &'static str {
119 determine_os(env::consts::OS)
120}
121
122// Helper function for testability
123pub(crate) fn determine_os(os: &str) -> &str {
124 match os {
125 "linux" => "linux",
126 "macos" => "macOS",
127 "windows" => {
128 if cfg!(target_os = "windows") {
129 "win32"
130 } else {
131 "win64"
132 }
133 }
134 _ => os,
135 }
136}
137
138/// Converts a byte slice to a String using the specified encoding.
139///
140/// # Arguments
141///
142/// * `b` - A byte slice (`&[u8]`) that needs to be converted to a String.
143///
144/// # Returns
145///
146/// A `Result<String, std::str::Utf8Error>` where:
147/// - `Ok(String)` contains the decoded string if successful.
148/// - `Err(std::str::Utf8Error)` if the byte slice is not valid UTF-8.
149///
150/// # Examples
151///
152/// ```
153/// use keeper_secrets_manager_core::utils::bytes_to_string;
154/// let bytes = b"Hello, world!";
155/// let result = bytes_to_string(bytes);
156/// assert_eq!(result.unwrap(), "Hello, world!");
157/// ```
158pub fn bytes_to_string(b: &[u8]) -> Result<String, KSMRError> {
159 // let bytes_sorted = unpad_data(b)?;
160 let string_of_bytes: String = str::from_utf8(b)
161 .map_err(|e| KSMRError::DecodeError(e.to_string()))?
162 .to_string();
163 Ok(string_of_bytes)
164}
165
166pub fn bytes_to_string_unpad(b: &[u8]) -> Result<String, KSMRError> {
167 let bytes_sorted = unpad_data(b)?;
168 let string_of_bytes: String = str::from_utf8(&bytes_sorted)
169 .map_err(|e| KSMRError::DecodeError(e.to_string()))?
170 .to_string();
171 Ok(string_of_bytes)
172}
173
174/// Converts a byte slice to an integer using big-endian byte order.
175///
176/// # Arguments
177///
178/// * `b` - A byte slice (`&[u8]`) that needs to be converted to an integer.
179///
180/// # Returns
181///
182/// An `Option<u64>` where:
183/// - `Some(u64)` contains the converted integer if successful.
184/// - `None` if the byte slice is empty or too long to fit in a u64.
185///
186/// # Examples
187///
188/// ```
189/// use keeper_secrets_manager_core::utils::bytes_to_int;
190/// use num_bigint::BigUint;
191/// let bytes = [0x00, 0x01, 0x02, 0x03];
192/// let result = bytes_to_int(&bytes);
193/// assert_eq!(result.unwrap(), BigUint::from(66051u64));
194/// ```
195pub fn bytes_to_int(b: &[u8]) -> Result<BigUint, KSMRError> {
196 Ok(BigUint::from_bytes_be(b))
197}
198
199/// Converts a byte slice to a Base64-encoded string.
200///
201/// # Arguments
202///
203/// * `b` - A byte slice (`&[u8]`) that needs to be converted to a Base64 string.
204///
205/// # Returns
206///
207/// A `String` containing the Base64-encoded representation of the input byte slice.
208///
209/// # Examples
210///
211/// ```
212/// use keeper_secrets_manager_core::utils::bytes_to_base64;
213/// let bytes = b"Hello, world!";
214/// let result = bytes_to_base64(bytes);
215/// assert_eq!(result, "SGVsbG8sIHdvcmxkIQ==");
216/// ```
217pub fn bytes_to_base64(b: &[u8]) -> String {
218 STANDARD.encode(b)
219}
220
221/// Converts a Base64-encoded string to a byte vector.
222///
223/// # Arguments
224///
225/// * `s` - A string slice (`&str`) that contains the Base64-encoded data.
226///
227/// # Returns
228///
229/// A `Result<Vec<u8>, base64::DecodeError>` where:
230/// - `Ok(Vec<u8>)` contains the decoded byte vector if successful.
231/// - `Err(base64::DecodeError)` if the input string is not valid Base64.
232///
233/// # Examples
234///
235/// ```
236/// use keeper_secrets_manager_core::utils::base64_to_bytes;
237/// let encoded = "SGVsbG8sIHdvcmxkIQ==";
238/// let result = base64_to_bytes(encoded);
239/// assert_eq!(result.unwrap(), b"Hello, world!");
240/// ```
241pub fn base64_to_bytes(s: &str) -> Result<Vec<u8>, KSMRError> {
242 let decode_confed_str = s.to_string().replace("+", "-").replace("/", "_");
243
244 let decoded_bytes = BASE64_URL_SAFE
245 .decode(decode_confed_str)
246 .map_err(|e| KSMRError::DecodeError(e.to_string()))?;
247
248 Ok(decoded_bytes)
249}
250
251/// Converts a Base64-encoded string to a UTF-8 string.
252///
253/// # Arguments
254///
255/// * `b64s` - A string slice (`&str`) containing the Base64-encoded data.
256///
257/// # Returns
258///
259/// A `Result<String, KSMRError>` where:
260/// - `Ok(String)` contains the decoded UTF-8 string if successful.
261/// - `Err(KSMRError)` if there is an error during decoding.
262///
263/// # Examples
264///
265/// ```
266/// use keeper_secrets_manager_core::utils::base64_to_string;
267/// use base64::{ engine::general_purpose::STANDARD, Engine as _};
268/// let encoded = STANDARD.encode("hello");
269/// let result = base64_to_string(&encoded.to_string());
270/// assert_eq!(result.unwrap(), "hello");
271/// ```
272pub fn base64_to_string(b64s: &str) -> Result<String, KSMRError> {
273 let decoded_bytes = STANDARD
274 .decode(b64s)
275 .map_err(|_| KSMRError::DecodeError("Failed to decode Base64 string".to_string()))?;
276
277 let decoded_string = String::from_utf8(decoded_bytes)
278 .map_err(|_| KSMRError::DecodeError("Failed to convert bytes to UTF-8".to_string()))?;
279
280 Ok(decoded_string)
281}
282
283pub fn base64_to_string_lossy(b64s: &str) -> Result<String, KSMRError> {
284 let decoded_bytes = STANDARD
285 .decode(b64s)
286 .map_err(|_| KSMRError::DecodeError("Failed to decode Base64 string".to_string()))?;
287
288 let decoded_string = String::from_utf8_lossy(&decoded_bytes).to_string();
289
290 Ok(decoded_string)
291}
292
293/// Converts a string to a byte vector using UTF-8 encoding.
294///
295/// # Arguments
296///
297/// * `s` - A string slice (`&str`) to be converted to bytes.
298///
299/// # Returns
300///
301/// A `Vec<u8>` containing the UTF-8 encoded bytes of the input string.
302///
303/// # Examples
304///
305/// ```
306/// use keeper_secrets_manager_core::utils::string_to_bytes;
307/// let input = "Hello, world!";
308/// let bytes = string_to_bytes(input);
309/// assert_eq!(bytes, b"Hello, world!");
310/// ```
311pub fn string_to_bytes(s: &str) -> Vec<u8> {
312 s.as_bytes().to_vec() // Convert the string slice to a byte vector
313}
314
315/// Converts a URL-safe Base64-encoded string to a byte vector.
316///
317/// # Arguments
318///
319/// * `s` - A string slice (`&str`) containing the URL-safe Base64-encoded data.
320///
321/// # Returns
322///
323/// A `Result<Vec<u8>, base64::DecodeError>` where:
324/// - `Ok(Vec<u8>)` contains the decoded byte vector if successful.
325/// - `Err(base64::DecodeError)` if the input string is not valid Base64.
326///
327/// # Examples
328///
329/// ```
330/// use keeper_secrets_manager_core::utils::url_safe_str_to_bytes;
331/// let encoded = "SGVsbG8sIFdvcmxkIQ"; // URL-safe Base64 string
332/// let result = url_safe_str_to_bytes(encoded);
333/// assert_eq!(result.unwrap(), b"Hello, World!");
334/// ```
335pub fn url_safe_str_to_bytes(s: &str) -> Result<Vec<u8>, crate::custom_error::KSMRError> {
336 // // Add padding manually if necessary
337 // let padded_str = if s.len() % 4 != 0 {
338 // format!("{}{}", s, "=".repeat(4 - s.len() % 4))
339 // } else {
340 // s.to_string()
341 // };
342 URL_SAFE_NO_PAD
343 .decode(s)
344 .map_err(|e| KSMRError::DecodeError(e.to_string()))
345}
346
347/// Converts a URL-safe Base64-encoded string to a u64 integer.
348///
349/// # Arguments
350///
351/// * `s` - A string slice (`&str`) containing the URL-safe Base64-encoded data.
352///
353/// # Returns
354///
355/// A `Result<u64, String>` where:
356/// - `Ok(u64)` contains the converted integer if successful.
357/// - `Err(String)` if the input string is not valid Base64 or if the conversion fails.
358/// # Examples
359///
360/// ```
361/// use keeper_secrets_manager_core::utils::url_safe_str_to_int;
362/// use num_bigint::BigUint;
363/// let encoded = "4oCU"; // URL-safe Base64 string
364/// let decoded = url_safe_str_to_int(encoded).unwrap();
365/// assert_eq!(decoded, BigUint::from(14844052u64));
366pub fn url_safe_str_to_int(s: &str) -> Result<BigUint, KSMRError> {
367 let bytes_of_str = url_safe_str_to_bytes(s)?;
368 bytes_to_int(bytes_of_str.as_slice()).map_err(|e| KSMRError::DecodeError(e.to_string()))
369}
370
371/// Generates a vector of random bytes.
372///
373/// # Arguments
374///
375/// * `length` - The number of random bytes to generate.
376///
377/// # Returns
378///
379/// A `Vec<u8>` containing the generated random bytes.
380///
381/// # Examples
382///
383/// ```
384/// use keeper_secrets_manager_core::utils::generate_random_bytes;
385/// let random_bytes = generate_random_bytes(16);
386/// assert_eq!(random_bytes.len(), 16); // Should be 16 bytes long
387/// ```
388pub fn generate_random_bytes(length: usize) -> Vec<u8> {
389 CryptoUtils::generate_random_bytes(length)
390}
391
392/// Generates UID bytes with specific bit conditions.
393///
394/// # Returns
395///
396/// A `Vec<u8>` containing the generated UID bytes.
397///
398/// # Examples
399///
400/// ```
401/// use keeper_secrets_manager_core::utils::generate_uid_bytes;
402/// let uid = generate_uid_bytes();
403/// assert_eq!(uid.len(), 16); // Should generate a UID of 16 bytes
404/// // Further checks can be added based on expected UID properties
405/// ```
406pub fn generate_uid_bytes() -> Vec<u8> {
407 let dash = [0xf8, 0x7f]; // Represents [11111000, 01111111]
408 let mut uid_bytes: Vec<u8> = Vec::new();
409
410 for _ in 0..8 {
411 uid_bytes = generate_random_bytes(16);
412 if dash[0] & uid_bytes[0] != dash[0] {
413 break;
414 }
415 }
416
417 if dash[0] & uid_bytes[0] == dash[0] {
418 uid_bytes[0] &= dash[1];
419 }
420
421 uid_bytes
422}
423
424pub fn generate_uid() -> String {
425 let uid_bytes = generate_uid_bytes();
426 CryptoUtils::bytes_to_url_safe_str(&uid_bytes)
427}
428
429/// Converts a dictionary to a JSON string with pretty formatting.
430///
431/// # Arguments
432///
433/// * `dictionary` - A reference to a serializable object that can be converted to JSON.
434///
435/// # Returns
436///
437/// A `Result<String, serde_json::Error>` containing the formatted JSON string if successful,
438/// or an error if serialization fails.
439///
440/// # Examples
441///
442/// ```
443/// use keeper_secrets_manager_core::utils::dict_to_json;
444/// let dictionary = [("key1", "value1"), ("key2", "value2")].iter().cloned().collect::<std::collections::HashMap<_, _>>();
445/// let json = dict_to_json(&dictionary).unwrap();
446/// println!("{}", json); // Outputs the JSON representation of the dictionary
447/// ```
448pub fn dict_to_json<T: Serialize>(dictionary: &T) -> serde_json::Result<String> {
449 serde_json::to_string_pretty(dictionary)
450}
451
452/// Converts a JSON string to a dictionary (HashMap).
453///
454/// # Arguments
455///
456/// * `json_str` - A string slice containing the JSON data.
457///
458/// # Returns
459///
460/// An `Option<Value>` which will be `Some(Value)` containing the parsed JSON
461/// if successful, or `None` if parsing fails.
462///
463/// # Examples
464///
465/// ```
466/// use keeper_secrets_manager_core::utils::json_to_dict;
467/// let json_str = r#"{"key1": "value1", "key2": "value2"}"#;
468/// let dict = json_to_dict(json_str);
469/// assert!(dict.is_some());
470/// ```
471pub fn json_to_dict(json_str: &str) -> Option<HashMap<String, Value>> {
472 let return_value = serde_json::from_str(json_str).map_err(|err| {
473 warn!("JSON decode error: {}", err);
474 });
475 // return Some(return_value);
476 match return_value {
477 Ok(map) => Some(map),
478 Err(err) => {
479 warn!("JSON decode error: {:?}", err);
480 None
481 }
482 }
483}
484
485/// Returns the current time in milliseconds since the Unix epoch.
486///
487/// This function retrieves the current UTC time and converts it into
488/// milliseconds since January 1, 1970 (the Unix epoch).
489///
490/// # Returns
491///
492/// An `i64` representing the current time in milliseconds.
493///
494/// # Examples
495///
496/// ```
497/// use keeper_secrets_manager_core::utils::now_milliseconds;
498/// let millis = now_milliseconds();
499/// println!("Current time in milliseconds: {}", millis);
500/// ```
501///
502/// In the example above, the function returns the current time in milliseconds,
503/// which can be used for timestamping events or measuring time intervals.
504pub fn now_milliseconds() -> i64 {
505 Utc::now().timestamp_millis()
506}
507#[cfg(feature = "totp")]
508/// Represents a TOTP code along with its time left and period.
509#[derive(Debug, Clone)]
510pub struct TotpCode {
511 code: String,
512 time_left: u64, // Assuming time_left is in seconds
513 period: u64, // Assuming period is also in seconds
514}
515
516#[cfg(feature = "totp")]
517impl TotpCode {
518 /// Creates a new `TotpCode`.
519 ///
520 /// # Arguments
521 ///
522 /// * `code` - A string representing the TOTP code.
523 /// * `time_left` - The time left until the code expires, in seconds.
524 /// * `period` - The period for which the TOTP code is valid, in seconds.
525 ///
526 /// # Returns
527 ///
528 /// A new instance of `TotpCode`.
529 pub fn new(code: String, time_left: u64, period: u64) -> Self {
530 TotpCode {
531 code,
532 time_left,
533 period,
534 }
535 }
536
537 /// Returns the TOTP code.
538 pub fn get_code(&self) -> &str {
539 &self.code
540 }
541
542 /// Returns the time left.
543 pub fn get_time_left(&self) -> u64 {
544 self.time_left
545 }
546
547 /// Returns the period.\
548 pub fn get_period(&self) -> u64 {
549 self.period
550 }
551}
552
553/// Generates a TOTP code from a given otp auth URL.
554///
555/// # Arguments
556///
557/// * `url` - A string slice containing the otp auth URL.
558///
559/// # Returns
560///
561/// A `Result<TotpCode, String>` which contains the generated TOTP code
562/// if successful, or an error message if parsing or generation fails.
563///
564/// # Examples
565///
566/// ```
567/// use keeper_secrets_manager_core::utils::get_totp_code;
568/// let url = "otpauth://totp/Example?secret=JBSWY3DPEHPK3PXP&issuer=Example";
569/// match get_totp_code(url) {
570/// Ok(totp_code) => println!("Generated TOTP code: {:?}", totp_code),
571/// Err(e) => println!("Error: {}", e),
572/// }
573/// ```
574#[cfg(feature = "totp")]
575pub fn get_totp_code(url: &str) -> Result<TotpCode, KSMRError> {
576 let comp = Url::parse(url).map_err(|_| KSMRError::TOTPError("Invalid URL".to_string()))?;
577 if comp.scheme() != "otpauth" {
578 return Err(KSMRError::TOTPError("Not an otpauth URI".to_string()));
579 }
580
581 let mut secret = None;
582 let mut algorithm = "SHA1".to_string();
583 let mut digits = 6;
584 let mut period = 30;
585 let mut counter = 0;
586
587 // Parse URL query string
588 let query_pairs = parse(comp.query().unwrap_or("").as_bytes());
589 for (key, value) in query_pairs {
590 match key.as_ref() {
591 "secret" => secret = Some(value.into_owned()),
592 "algorithm" => algorithm = value.into_owned().to_uppercase(),
593 "digits" => {
594 if let Ok(num) = value.parse::<u32>() {
595 if num > 0 && num < 10 {
596 digits = num;
597 } else {
598 return Err(KSMRError::TOTPError(
599 "TOTP Digits may only be 6, 7, or 8".to_string(),
600 ));
601 }
602 }
603 }
604 "period" => {
605 if let Ok(num) = value.parse::<u32>() {
606 if num > 0 {
607 period = num;
608 }
609 }
610 }
611 "counter" => {
612 if let Ok(num) = value.parse::<u32>() {
613 if num > 0 {
614 counter = num;
615 }
616 }
617 }
618 _ => {}
619 }
620 }
621
622 // Validate parameters
623 let secret = secret
624 .ok_or(KSMRError::TOTPError(
625 "TOTP secret not found in URI".to_string(),
626 ))?
627 .to_ascii_uppercase();
628 let decoded_key_option = BASE32.decode(secret.as_bytes());
629 let key = match decoded_key_option {
630 Ok(decoded_key) => decoded_key,
631 Err(err) => Err(KSMRError::DecodeError(format!(
632 "Invalid TOTP secret: {}",
633 err
634 )))?,
635 };
636
637 let tm_base = if counter > 0 {
638 counter
639 } else {
640 Utc::now().timestamp() as u32
641 };
642 let tm = tm_base / period;
643 let msg = (tm as u64).to_be_bytes();
644
645 let digest: Vec<u8> = match algorithm.as_str() {
646 "SHA1" => {
647 let mut hmac = Hmac::<Sha1>::new_from_slice(&key)
648 .map_err(|_| KSMRError::TOTPError("Failed to create HMAC".to_string()))?;
649 hmac.update(&msg);
650 hmac.finalize().into_bytes().to_vec()
651 }
652 "SHA256" => {
653 let mut hmac = Hmac::<Sha256>::new_from_slice(&key)
654 .map_err(|_| KSMRError::TOTPError("Failed to create HMAC".to_string()))?;
655 hmac.update(&msg);
656 hmac.finalize().into_bytes().to_vec()
657 }
658 "SHA512" => {
659 let mut hmac = Hmac::<Sha512>::new_from_slice(&key)
660 .map_err(|_| KSMRError::TOTPError("Failed to create HMAC".to_string()))?;
661 hmac.update(&msg);
662 hmac.finalize().into_bytes().to_vec()
663 }
664 _ => {
665 return Err(KSMRError::TOTPError(format!(
666 "Invalid algorithm: {}",
667 algorithm
668 )))
669 }
670 };
671
672 let offset = (digest.last().unwrap() & 0x0f) as usize;
673 let base = &digest[offset..offset + 4];
674 let code_int = ((base[0] & 0x7f) as u32) << 24
675 | (base[1] as u32) << 16
676 | (base[2] as u32) << 8
677 | (base[3] as u32);
678 let code = format!(
679 "{:0width$}",
680 code_int % 10u32.pow(digits),
681 width = digits as usize
682 );
683
684 let elapsed = tm_base % period; // time elapsed in current period in seconds
685 let ttl = period - elapsed; // time to live in seconds
686
687 Ok(TotpCode::new(code, ttl as u64, period as u64))
688}
689
690pub fn get_otp_url_from_value_obj(val: serde_json::Value) -> Result<String, KSMRError> {
691 let otp_value = match val.is_array() {
692 true => val.as_array().unwrap()[0][0].clone(),
693 false => {
694 return Err(KSMRError::RecordDataError(
695 "otpCode or otp field is not an array".to_string(),
696 ))
697 }
698 };
699
700 let url_retrieved = match otp_value.is_string() {
701 true => otp_value.as_str().unwrap().to_string(),
702 false => {
703 return Err(KSMRError::RecordDataError(
704 "otpCode or otp field is not a string".to_string(),
705 ))
706 }
707 };
708
709 Ok(url_retrieved)
710}
711/// Generates a random sample of characters from a given string.
712///
713/// # Parameters
714/// - `sample_length`: The number of characters to sample.
715/// - `sample_string`: The string from which to sample characters.
716///
717/// # Returns
718/// A `String` containing the sampled characters.
719///
720/// # Errors
721/// Returns an error if `sample_length` is negative or if `sample_string` is empty.
722///
723/// # Example
724/// ```
725/// use keeper_secrets_manager_core::utils::random_sample;
726/// let result = random_sample(10, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").unwrap();
727/// println!("Random Sample: {}", result);
728/// ```
729pub fn random_sample(
730 sample_length: usize,
731 sample_string: &str,
732) -> Result<String, Box<dyn std::error::Error>> {
733 // Validate inputs
734 if sample_length == 0 {
735 return Ok(String::new());
736 }
737
738 if sample_string.is_empty() {
739 return Err("sample_string must not be empty".into());
740 }
741
742 let mut rng = rand::thread_rng();
743 let mut sample = String::new();
744
745 for _ in 0..sample_length {
746 let char = sample_string
747 .chars()
748 .choose(&mut rng)
749 .ok_or("Failed to choose a character")?;
750 sample.push(char);
751 }
752
753 Ok(sample)
754}
755
756/// Gets the current Windows user SID and name.
757///
758/// # Returns
759/// A tuple containing the SID and username, or (None, None) if there was an error.
760///
761/// # Example
762/// ```
763/// use std::process::{Command, Output};
764/// use std::option::Option;
765/// use keeper_secrets_manager_core::utils::get_windows_user_sid_and_name;
766/// let (sid, username) = get_windows_user_sid_and_name::<fn() -> Output>(None);
767/// println!("SID: {:?}, Username: {:?}", sid, username);
768/// ```
769#[cfg(target_os = "windows")]
770pub fn get_windows_user_sid_and_name<F>(command: Option<F>) -> (Option<String>, Option<String>)
771where
772 F: Fn() -> Output,
773{
774 let output = match command {
775 Some(comm) => comm(),
776 None => _default_command(),
777 };
778
779 if output.status.success() {
780 let stdout = String::from_utf8_lossy(&output.stdout);
781 let lines: Vec<&str> = stdout.lines().collect();
782 if let Some(last_line) = lines.last() {
783 let parts: Vec<&str> = last_line.split('\\').collect();
784 if let Some(username_sid) = parts.last() {
785 let username_sid_string = username_sid.to_string();
786 let split_parts: Vec<&str> = username_sid_string.split(" ").collect();
787 // let mut parts_split = username_sid.split_ascii_whitespace();
788 // let username = parts_split.next().unwrap_or("");
789 // let sid = parts_split.next().unwrap_or("");
790 let username = split_parts[0].to_string();
791 let sid = split_parts[1].to_string();
792 return (Some(sid), Some(username));
793 }
794 }
795 } else {
796 eprintln!("Failed to execute 'whoami.exe'");
797 }
798
799 (None, None)
800}
801
802#[cfg(target_os = "windows")]
803fn _default_command() -> Output {
804 Command::new("whoami.exe")
805 .arg("/user")
806 .output()
807 .expect("Failed to execute whoami.exe")
808}
809
810#[cfg(not(target_os = "windows"))]
811fn _default_command() -> Output {
812 // This can be a dummy output or an error for non-Windows platforms.
813
814 use std::os::unix::process::ExitStatusExt;
815 Output {
816 status: std::process::ExitStatus::from_raw(1),
817 stdout: Vec::new(),
818 stderr: Vec::new(),
819 }
820}
821
822/**
823Sets the configuration mode for the specified file, adjusting its permissions
824according to the operating system's conventions. On Windows, it uses `icacls`
825to set file permissions, while on Linux/MacOS, it sets the permissions to 0600.
826
827# Arguments
828
829* `file` - A string slice that holds the path to the configuration file.
830
831# Returns
832
833This function returns `Ok(())` if the mode is set successfully, or an `io::Error`
834if an error occurs.
835
836*/
837pub fn set_config_mode(file: &str) -> Result<(), io::Error> {
838 // Check if we should skip setting the mode
839 if let Ok(skip_mode) = env::var("KSM_CONFIG_SKIP_MODE") {
840 if skip_mode.to_lowercase() == "true" {
841 return Ok(());
842 }
843 }
844
845 // For Windows, use icacls commands
846 #[cfg(target_os = "windows")]
847 {
848 let sid = match get_windows_user_sid_and_name::<fn() -> Output>(None) {
849 (Some(sid), _) => sid,
850 _ => {
851 return Err(io::Error::new(
852 io::ErrorKind::Other,
853 "Failed to get user SID",
854 ))
855 }
856 };
857
858 // Commands to set the file permissions
859 let commands = vec![
860 format!(r#"icacls "{}" /reset"#, file),
861 format!(r#"icacls "{}" /inheritance:r"#, file),
862 format!(r#"icacls "{}" /remove:g Everyone:F"#, file),
863 format!(r#"icacls "{}" /grant:r Administrators:F"#, file),
864 format!(r#"icacls "{}" /grant:r "{}:F""#, file, sid),
865 ];
866
867 for command in commands {
868 let output = Command::new("cmd").args(&["/C", &command]).output()?;
869
870 match output.status.code() {
871 Some(2) => {
872 return Err(io::Error::new(
873 io::ErrorKind::NotFound,
874 format!("Cannot find configuration file {}", file),
875 ))
876 }
877 Some(5) => {
878 return Err(io::Error::new(
879 io::ErrorKind::PermissionDenied,
880 format!("Access denied to configuration file {}", file),
881 ))
882 }
883 Some(1332) => {
884 debug!("{} {}", "Failed to set some ACL permissions: {}", command);
885 continue; // Skip localized group/user names error
886 }
887 Some(_) if !output.status.success() => {
888 let message = format!(
889 "Could not change the ACL for file '{}'. Set the environmental variable 'KSM_CONFIG_SKIP_MODE' to 'TRUE' to skip setting the ACL mode.",
890 file
891 );
892 let stderr = String::from_utf8_lossy(&output.stderr);
893 let full_message = if !stderr.is_empty() {
894 format!("{}: {}", message, stderr.trim())
895 } else {
896 format!("{}.", message)
897 };
898 return Err(io::Error::new(
899 io::ErrorKind::PermissionDenied,
900 full_message,
901 ));
902 }
903 _ => {}
904 }
905 }
906 }
907 #[cfg(not(target_os = "windows"))]
908 {
909 // On Linux/MacOS, set file permissions to 0600
910 let permissions = fs::metadata(file)?.permissions();
911 let mut new_permissions = permissions;
912 new_permissions.set_mode(0o600);
913 fs::set_permissions(file, new_permissions)?;
914 }
915
916 Ok(())
917}
918
919//This function runs only on windows and this unit test runs only on windows
920/// Retrieves localized account names for known administrative accounts on Windows.
921///
922/// This function uses the Win32 API to fetch account names for the local system
923/// and built-in administrators. It returns a vector of strings containing the
924/// localized names.
925///
926/// # Errors
927///
928/// This function returns an error if it fails to create a well-known SID,
929/// look up the account names, or execute the command to convert the names
930/// to the console's code page.
931///
932/// # Example
933///
934/// ```ignore
935/// fn main() -> Result<(), u32> {
936/// match _populate_windows_localized_admin_names_win32api() {
937/// Ok(localized_admins) => {
938/// for admin in localized_admins {
939/// println!("Localized Admin: {}", admin);
940/// }
941/// },
942/// Err(err) => eprintln!("Error retrieving localized admin names: {}", err),
943/// }
944/// Ok(())
945/// }
946/// ```
947///
948/// # Test
949///
950/// This test will run if the target operating system is Windows and the
951/// function is expected to return a non-empty list of localized admin names.
952///
953/// ```ignore
954/// #[cfg(test)]
955/// mod tests {
956/// use super::*; // Ensure the test module has access to the function
957///
958/// #[test]
959/// #[cfg(target_os = "windows")] // Only run this test on Windows
960/// fn test_populate_windows_localized_admin_names_win32api() {
961/// let result = _populate_windows_localized_admin_names_win32api();
962/// assert!(result.is_ok(), "Function should return Ok on success");
963/// let localized_admins = result.unwrap();
964/// assert!(!localized_admins.is_empty(), "Should return at least one localized admin name");
965/// }
966/// }
967/// ```
968#[cfg(target_os = "windows")]
969fn _populate_windows_localized_admin_names_win32api() -> Result<Vec<String>, u32> {
970 use std::ffi::OsString; // Make sure to import OsString
971 use std::os::windows::ffi::OsStringExt; // Import the OsStringExt trait
972 use std::ptr;
973 use winapi::shared::winerror::{ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_PARAMETER};
974 use winapi::um::errhandlingapi::GetLastError;
975 use winapi::um::securitybaseapi::CreateWellKnownSid;
976 use winapi::um::winbase::LookupAccountSidW;
977
978 // Define WellKnownSidType manually
979 #[allow(non_camel_case_types)]
980 #[repr(u32)]
981 #[derive(Clone)]
982 pub enum WellKnownSidType {
983 WinLocalSystemSid = 22,
984 WinBuiltinAdministratorsSid = 26,
985 }
986
987 // Helper function to convert a wide string (UTF-16) to a Rust String
988 fn wide_to_string(wide: &[u16]) -> String {
989 let os_string = OsString::from_wide(wide);
990 os_string.to_string_lossy().into_owned()
991 }
992
993 // Function to get account name for a specific SID type
994 fn get_account_name(sid_type: WellKnownSidType) -> Result<(String, String), u32> {
995 let mut sid_size = 256;
996 let mut sid = vec![0u8; sid_size as usize];
997
998 unsafe {
999 // CreateWellKnownSid
1000 if CreateWellKnownSid(
1001 sid_type.clone() as u32,
1002 ptr::null_mut(),
1003 sid.as_mut_ptr() as *mut _,
1004 &mut sid_size,
1005 ) == 0
1006 {
1007 let error = GetLastError();
1008 if error == ERROR_INSUFFICIENT_BUFFER || error == ERROR_INVALID_PARAMETER {
1009 sid = vec![0u8; sid_size as usize];
1010 if CreateWellKnownSid(
1011 sid_type as u32,
1012 ptr::null_mut(),
1013 sid.as_mut_ptr() as *mut _,
1014 &mut sid_size,
1015 ) == 0
1016 {
1017 return Err(GetLastError());
1018 }
1019 } else {
1020 return Err(error);
1021 }
1022 }
1023
1024 // LookupAccountSidW to get the size needed for the name and domain
1025 let mut name_size = 0;
1026 let mut domain_size = 0;
1027 let mut sid_name_use = 0;
1028 LookupAccountSidW(
1029 ptr::null(),
1030 sid.as_mut_ptr() as *mut _,
1031 ptr::null_mut(),
1032 &mut name_size,
1033 ptr::null_mut(),
1034 &mut domain_size,
1035 &mut sid_name_use,
1036 );
1037
1038 let error = GetLastError();
1039 if error != ERROR_INSUFFICIENT_BUFFER {
1040 return Err(error);
1041 }
1042
1043 // Allocate buffers for the account name and domain name
1044 let mut name = vec![0u16; name_size as usize];
1045 let mut domain = vec![0u16; domain_size as usize];
1046
1047 // LookupAccountSidW to actually get the name and domain
1048 if LookupAccountSidW(
1049 ptr::null(),
1050 sid.as_mut_ptr() as *mut _,
1051 name.as_mut_ptr(),
1052 &mut name_size,
1053 domain.as_mut_ptr(),
1054 &mut domain_size,
1055 &mut sid_name_use,
1056 ) == 0
1057 {
1058 return Err(GetLastError());
1059 }
1060
1061 // Convert wide strings to Rust Strings
1062 let domain_str = wide_to_string(&domain);
1063 let name_str = wide_to_string(&name);
1064
1065 Ok((domain_str, name_str))
1066 }
1067 }
1068
1069 let mut localized_admins = Vec::new();
1070 let mut admins = Vec::new();
1071
1072 // Retrieve account names for specific well-known SIDs
1073 if let Ok((_, name)) = get_account_name(WellKnownSidType::WinLocalSystemSid) {
1074 admins.push(name);
1075 }
1076
1077 if let Ok((_, name)) = get_account_name(WellKnownSidType::WinBuiltinAdministratorsSid) {
1078 admins.push(name);
1079 }
1080
1081 // Convert WMI names (admins) to the console's code page using a shell command (like "cmd /c")
1082 if !admins.is_empty() {
1083 let mut cmd = String::from("echo.");
1084 for admin in &admins {
1085 cmd.push_str(&format!(" & echo {}", admin));
1086 }
1087
1088 // Execute the command in the shell
1089 let output = std::process::Command::new("cmd")
1090 .args(&["/C", &cmd])
1091 .output()
1092 .expect("Failed to execute command");
1093
1094 if output.status.success() {
1095 let output_lines = output.stdout.split(|&b| b == b'\n');
1096 for line in output_lines {
1097 if !line.is_empty() {
1098 localized_admins.push(String::from_utf8_lossy(line).trim().to_string());
1099 }
1100 }
1101 }
1102 }
1103
1104 Ok(localized_admins)
1105}
1106
1107#[derive(Debug)]
1108pub enum ConfigError {
1109 PermissionDenied(String),
1110 FileNotFound(String),
1111 GeneralError(String),
1112}
1113
1114/// This function checks the permissions of a given configuration file.
1115/// On Windows, it uses the `icacls` command to verify permissions.
1116/// On Unix-like systems (Linux, macOS), it checks the file mode and ensures that
1117/// only the owner has access.
1118///
1119/// The function will skip permission checking if the `KSM_CONFIG_SKIP_MODE` environment
1120/// variable is set to `TRUE`.
1121///
1122/// On Windows, if access is denied or the file is missing, specific errors are returned.
1123/// The function also checks for warnings about overly permissive access modes.
1124///
1125/// # Errors
1126///
1127/// Returns:
1128/// - `ConfigError::PermissionDenied` if the file is accessible by users other than the owner.
1129/// - `ConfigError::FileNotFound` if the file does not exist.
1130/// - `ConfigError::GeneralError` if there are other issues, such as executing the `icacls` command.
1131///
1132/// # Example (Unix-like systems)
1133/// ```ignore
1134/// use std::fs;
1135/// #[cfg(unix)]
1136/// use std::os::unix::fs::PermissionsExt;
1137/// use keeper_secrets_manager_core::utils::check_config_mode;
1138/// use keeper_secrets_manager_core::utils::ConfigError;
1139///
1140/// // Create a file and set restrictive permissions (only owner can access).
1141/// let file_path = "client-config.json";
1142/// fs::File::create(file_path).unwrap();
1143/// let mut perms = fs::metadata(file_path).unwrap().permissions();
1144/// perms.set_mode(0o600); // Owner can read/write
1145/// fs::set_permissions(file_path, perms).unwrap();
1146///
1147/// // Run the function
1148/// match check_config_mode(file_path) {
1149/// Ok(true) => println!("Permissions are correctly set."),
1150/// Ok(false) => println!("Permissions are too open."),
1151/// Err(ConfigError::PermissionDenied(err)) => eprintln!("Permission denied: {}", err),
1152/// Err(ConfigError::FileNotFound(err)) => eprintln!("File not found: {}", err),
1153/// Err(ConfigError::GeneralError(err)) => eprintln!("General error: {}", err),
1154/// _ => eprintln!("Unknown error."),
1155/// }
1156/// ```
1157///
1158/// # Example (Windows)
1159/// ```rust,ignore
1160/// use keeper_secrets_manager_core::utils::check_config_mode;
1161/// use keeper_secrets_manager_core::utils::ConfigError;
1162///
1163/// let file_path = "client-config.json";
1164///
1165/// // Run the function
1166/// match check_config_mode(file_path) {
1167/// Ok(true) => println!("Permissions are correctly set."),
1168/// Ok(false) => println!("Permissions are too open."),
1169/// Err(ConfigError::PermissionDenied(err)) => eprintln!("Permission denied: {}", err),
1170/// Err(ConfigError::FileNotFound(err)) => eprintln!("File not found: {}", err),
1171/// Err(ConfigError::GeneralError(err)) => eprintln!("General error: {}", err),
1172/// _ => eprintln!("Unknown error."),
1173/// }
1174/// ```
1175pub fn check_config_mode(file: &str) -> Result<bool, ConfigError> {
1176 let skip_mode_check = env::var("KSM_CONFIG_SKIP_MODE")
1177 .unwrap_or("FALSE".to_string())
1178 .eq_ignore_ascii_case("TRUE");
1179
1180 if skip_mode_check {
1181 return Ok(true);
1182 }
1183
1184 #[cfg(target_os = "windows")]
1185 return check_windows_permissions(file);
1186
1187 #[cfg(not(target_os = "windows"))]
1188 return check_unix_permissions(file);
1189}
1190
1191#[cfg(target_os = "windows")]
1192fn check_windows_permissions(file: &str) -> Result<bool, ConfigError> {
1193 use std::process::Command;
1194
1195 // Execute the `icacls` command to check file permissions
1196 let output = Command::new("icacls")
1197 .arg(file)
1198 .output()
1199 .map_err(|e| ConfigError::GeneralError(format!("Error executing icacls: {}", e)))?;
1200
1201 if !output.status.success() {
1202 return match output.status.code() {
1203 Some(2) => Err(ConfigError::FileNotFound(file.to_string())),
1204 Some(5) => Err(ConfigError::PermissionDenied(file.to_string())),
1205 _ => Err(ConfigError::GeneralError(
1206 "Unknown error in icacls".to_string(),
1207 )),
1208 };
1209 }
1210
1211 // Additional checks for user permissions
1212 if !is_file_accessible(file) {
1213 return Err(ConfigError::PermissionDenied(format!(
1214 "Access denied to {}",
1215 file
1216 )));
1217 }
1218
1219 Ok(true)
1220}
1221
1222#[cfg(not(target_os = "windows"))]
1223fn check_unix_permissions(file: &str) -> Result<bool, ConfigError> {
1224 // Check if the file exists first
1225
1226 use std::path::Path;
1227 let file_path = Path::new(file);
1228 if !file_path.exists() {
1229 return Err(ConfigError::FileNotFound(file.to_string()));
1230 }
1231
1232 // Attempt to open the file to verify access permissions
1233 let metadata =
1234 fs::metadata(file_path).map_err(|_| ConfigError::FileNotFound(file.to_string()))?;
1235 if !is_file_accessible(file) {
1236 return Err(ConfigError::PermissionDenied(file.to_string()));
1237 }
1238 // Retrieve file mode and permissions for validation
1239 let permissions = metadata.permissions().mode();
1240 if permissions & 0o077 != 0 {
1241 eprintln!(
1242 "Warning: File permissions for {} are too open ({:o}). Consider setting to 0600.",
1243 file, permissions
1244 );
1245 return Err(ConfigError::PermissionDenied(format!(
1246 "File permissions too open for {}",
1247 file
1248 )));
1249 }
1250
1251 Ok(true)
1252}
1253
1254// Check if file is accessible
1255fn is_file_accessible(file: &str) -> bool {
1256 File::open(file).is_ok()
1257}
1258
1259#[cfg(feature = "password-gen")]
1260#[derive(Debug)]
1261pub struct PasswordOptions {
1262 length: usize,
1263 lowercase: Option<i32>,
1264 uppercase: Option<i32>,
1265 digits: Option<i32>,
1266 special_characters: Option<i32>,
1267 special_characterset: String,
1268}
1269
1270#[cfg(feature = "password-gen")]
1271impl PasswordOptions {
1272 /// Creates a new PasswordOptions with default values.
1273 pub fn new() -> Self {
1274 PasswordOptions {
1275 length: DEFAULT_PASSWORD_LENGTH,
1276 lowercase: None,
1277 uppercase: None,
1278 digits: None,
1279 special_characters: None,
1280 special_characterset: String::from(SPECIAL_CHARACTERS),
1281 }
1282 }
1283
1284 /// Set the password length.
1285 pub fn length(mut self, length: usize) -> Self {
1286 if length > 0 {
1287 self.length = length;
1288 } else {
1289 self.length = 32
1290 }
1291 self
1292 }
1293
1294 /// Set the minimum number of lowercase characters.
1295 pub fn lowercase(mut self, count: i32) -> Self {
1296 self.lowercase = Some(count);
1297 self
1298 }
1299
1300 /// Set the minimum number of uppercase characters.
1301 pub fn uppercase(mut self, count: i32) -> Self {
1302 self.uppercase = Some(count);
1303 self
1304 }
1305
1306 /// Set the minimum number of digits.
1307 pub fn digits(mut self, count: i32) -> Self {
1308 self.digits = Some(count);
1309 self
1310 }
1311
1312 /// Set the minimum number of special characters.
1313 pub fn special_characters(mut self, count: i32) -> Self {
1314 self.special_characters = Some(count);
1315 self
1316 }
1317
1318 /// Set the custom set of special characters.
1319 pub fn special_characterset(mut self, charset: String) -> Self {
1320 self.special_characterset = charset;
1321 self
1322 }
1323}
1324
1325#[cfg(feature = "password-gen")]
1326impl Default for PasswordOptions {
1327 fn default() -> Self {
1328 Self::new()
1329 }
1330}
1331
1332#[cfg(feature = "password-gen")]
1333/// Generates a new password based on the specified options.
1334///
1335/// The generated password will adhere to the constraints set by the
1336/// provided `PasswordOptions`. If the specified character counts exceed
1337/// the total desired password length, an error will be returned.
1338///
1339/// # Parameters
1340///
1341/// - `options`: An instance of `PasswordOptions` that defines the desired
1342/// characteristics of the password, such as length and minimum character
1343/// counts for lowercase, uppercase, digits, and special characters.
1344///
1345/// # Returns
1346///
1347/// - `Ok(String)`: A randomly generated password that meets the specified
1348/// criteria.
1349/// - `Err(String)`: An error message if the specified character counts
1350/// exceed the total password length or if there are any issues during
1351/// password generation.
1352///
1353/// # Example
1354///
1355/// ```rust
1356/// use keeper_secrets_manager_core::utils::generate_password_with_options;
1357/// use keeper_secrets_manager_core::utils::PasswordOptions;
1358///
1359/// let options = PasswordOptions::new()
1360/// .length(16)
1361/// .lowercase(4)
1362/// .uppercase(4)
1363/// .digits(4);
1364///
1365/// match generate_password_with_options(options) {
1366/// Ok(password) => println!("Generated Password: {}", password),
1367/// Err(err) => eprintln!("Error: {}", err),
1368/// }
1369/// ```
1370///
1371/// # Panics
1372///
1373/// This function does not panic, but will return an error if constraints are not met.
1374///
1375/// # Errors
1376///
1377/// - If the specified lowercase, uppercase, digits, and special characters
1378/// exceed the total length of the password, an error will be returned.
1379pub fn generate_password_with_options(options: PasswordOptions) -> Result<String, KSMRError> {
1380 let mut rng = thread_rng();
1381
1382 // Determine if using pure exact-count mode (ALL four character types explicitly set to exact values)
1383 // In pure exact mode, the length parameter is ignored and password length = sum of absolute values
1384 // None is treated as "minimum of 0" (include in extra pool), so it counts as minimum mode
1385 let all_explicit_exact = options.lowercase.is_some_and(|v| v <= 0)
1386 && options.uppercase.is_some_and(|v| v <= 0)
1387 && options.digits.is_some_and(|v| v <= 0)
1388 && options.special_characters.is_some_and(|v| v <= 0);
1389 let has_any_minimum_or_none = options.lowercase.is_none()
1390 || options.lowercase.is_some_and(|v| v > 0)
1391 || options.uppercase.is_none()
1392 || options.uppercase.is_some_and(|v| v > 0)
1393 || options.digits.is_none()
1394 || options.digits.is_some_and(|v| v > 0)
1395 || options.special_characters.is_none()
1396 || options.special_characters.is_some_and(|v| v > 0);
1397 let is_exact_mode = all_explicit_exact && !has_any_minimum_or_none;
1398
1399 // Collect the counts for each character type (use abs() to support negative values as exact counts)
1400 let lowercase_count = options.lowercase.map_or(0, |v| v.abs());
1401 let uppercase_count = options.uppercase.map_or(0, |v| v.abs());
1402 let digits_count = options.digits.map_or(0, |v| v.abs());
1403 let special_count = options.special_characters.map_or(0, |v| v.abs());
1404
1405 // Calculate the total number of specified characters
1406 let total_specified = lowercase_count + uppercase_count + digits_count + special_count;
1407
1408 // Determine target password length
1409 // In exact mode: use sum of absolute values
1410 // In minimum mode: use requested length parameter
1411 let target_length = if is_exact_mode {
1412 total_specified as usize
1413 } else {
1414 options.length
1415 };
1416
1417 // In exact mode, warn if length parameter will be ignored
1418 if is_exact_mode && (options.length as i32) != total_specified {
1419 warn!(
1420 "Exact character counts specified (negative values) - password length will be {} instead of requested {}",
1421 total_specified, options.length
1422 );
1423 }
1424
1425 // Calculate extra characters needed to reach target length
1426 let extra_count = if (target_length as i32) > total_specified {
1427 target_length as i32 - total_specified
1428 } else {
1429 if !is_exact_mode && (options.length as i32) < total_specified {
1430 warn!(
1431 "Specified character counts ({}) exceed password length ({}) - no extra characters will be added",
1432 total_specified, options.length
1433 );
1434 }
1435 0
1436 };
1437
1438 // Error check: only throw error if we have minimum requirements (positive values) that can't be met
1439 // If we have exact counts (negative values) that exceed length, just warn and use the exact counts
1440 let has_positive_values = options.lowercase.is_some_and(|v| v > 0)
1441 || options.uppercase.is_some_and(|v| v > 0)
1442 || options.digits.is_some_and(|v| v > 0)
1443 || options.special_characters.is_some_and(|v| v > 0);
1444 if has_positive_values && total_specified > (options.length as i32) {
1445 return Err(KSMRError::PasswordCreationError(format!(
1446 "The specified character counts ({}) exceed the total password length ({})!",
1447 total_specified, options.length
1448 )));
1449 }
1450
1451 // Build the extra character pool - only include character types with positive (minimum) values
1452 // Negative values (exact counts) are excluded from the extra pool
1453 let mut extra_chars = String::new();
1454 if options.lowercase.is_none() || options.lowercase.is_some_and(|v| v > 0) {
1455 extra_chars.push_str("abcdefghijklmnopqrstuvwxyz");
1456 }
1457 if options.uppercase.is_none() || options.uppercase.is_some_and(|v| v > 0) {
1458 extra_chars.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
1459 }
1460 if options.digits.is_none() || options.digits.is_some_and(|v| v > 0) {
1461 extra_chars.push_str("0123456789");
1462 }
1463 if options.special_characters.is_none() || options.special_characters.is_some_and(|v| v > 0) {
1464 extra_chars.push_str(&options.special_characterset);
1465 }
1466 if extra_chars.is_empty() {
1467 extra_chars.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
1468 extra_chars.push_str(SPECIAL_CHARACTERS);
1469 }
1470
1471 // Initialize the category map
1472 let category_map = vec![
1473 (lowercase_count as usize, "abcdefghijklmnopqrstuvwxyz"),
1474 (uppercase_count as usize, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
1475 (digits_count as usize, "0123456789"),
1476 (special_count as usize, &options.special_characterset),
1477 (extra_count.max(0) as usize, &extra_chars),
1478 ];
1479
1480 let mut password_list = Vec::new();
1481 for (count, chars) in category_map {
1482 let char_slice: Vec<char> = chars.chars().collect();
1483 let mut repeated_chars = char_slice.iter().cycle(); // Infinite repetition
1484 for _ in 0..count {
1485 if let Some(&sample) = repeated_chars.next() {
1486 password_list.push(sample);
1487 }
1488 }
1489 }
1490
1491 let mut remaining_length = target_length.saturating_sub(password_list.len());
1492
1493 while remaining_length > 0 {
1494 // Randomly select additional characters from the extra characters
1495 let extra_char_slice: Vec<char> = extra_chars.chars().collect();
1496 let additional_samples: Vec<char> = extra_char_slice
1497 .choose_multiple(&mut rng, remaining_length)
1498 .cloned()
1499 .collect();
1500
1501 password_list.extend(additional_samples);
1502 remaining_length = target_length.saturating_sub(password_list.len())
1503 }
1504 password_list.shuffle(&mut rng);
1505
1506 Ok(password_list.into_iter().collect())
1507}
1508
1509#[cfg(feature = "password-gen")]
1510pub fn generate_password() -> Result<String, KSMRError> {
1511 let password_options_default = PasswordOptions::new();
1512 generate_password_with_options(password_options_default)
1513}