use crate::{
crypto::{unpad_data, CryptoUtils},
custom_error::KSMRError,
};
use base64::{
engine::general_purpose::{STANDARD, URL_SAFE_NO_PAD},
prelude::BASE64_URL_SAFE,
Engine as _,
};
use chrono::Utc;
use core::str;
use data_encoding::BASE32;
use hmac::{Hmac, Mac};
use log::warn;
use num_bigint::BigUint;
use rand::{
seq::{IteratorRandom, SliceRandom},
thread_rng,
};
use serde::Serialize;
use serde_json::Value;
use sha1::Sha1;
use sha2::{Sha256, Sha512};
use std::process::Output;
use std::{collections::HashMap, option::Option};
use std::{env, io};
use url::{form_urlencoded::parse, Url};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
#[cfg(target_os = "windows")]
use log::debug;
use std::fs::File;
#[cfg(target_os = "windows")]
use std::process::Command;
#[cfg(unix)]
use std::fs;
pub const ALLOWED_WINDOWS_CONFIG_ADMINS: [&[u8]; 2] = [b"Administrators", b"SYSTEM"];
pub const ENCODING: &str = "UTF-8";
pub const SPECIAL_CHARACTERS: &str = r#"""!@#$%()+;<>=?[]{}^.,"""#;
pub const DEFAULT_PASSWORD_LENGTH: usize = 32;
pub fn str_to_bool(val: &str) -> Result<bool, String> {
let val = val.to_lowercase();
match val.as_str() {
"y" | "yes" | "t" | "true" | "on" | "1" => Ok(true),
"n" | "no" | "f" | "false" | "off" | "0" => Ok(false),
_ => Err(format!("invalid truth value {:?}", val)),
}
}
pub fn get_os() -> &'static str {
determine_os(env::consts::OS)
}
pub(crate) fn determine_os(os: &str) -> &str {
match os {
"linux" => "linux",
"macos" => "macOS",
"windows" => {
if cfg!(target_os = "windows") {
"win32"
} else {
"win64"
}
}
_ => os,
}
}
pub fn bytes_to_string(b: &[u8]) -> Result<String, KSMRError> {
let string_of_bytes: String = str::from_utf8(b)
.map_err(|e| KSMRError::DecodeError(e.to_string()))?
.to_string();
Ok(string_of_bytes)
}
pub fn bytes_to_string_unpad(b: &[u8]) -> Result<String, KSMRError> {
let bytes_sorted = unpad_data(b)?;
let string_of_bytes: String = str::from_utf8(&bytes_sorted)
.map_err(|e| KSMRError::DecodeError(e.to_string()))?
.to_string();
Ok(string_of_bytes)
}
pub fn bytes_to_int(b: &[u8]) -> Result<BigUint, KSMRError> {
Ok(BigUint::from_bytes_be(b))
}
pub fn bytes_to_base64(b: &[u8]) -> String {
STANDARD.encode(b)
}
pub fn base64_to_bytes(s: &str) -> Result<Vec<u8>, KSMRError> {
let decode_confed_str = s.to_string().replace("+", "-").replace("/", "_");
let decoded_bytes = BASE64_URL_SAFE
.decode(decode_confed_str)
.map_err(|e| KSMRError::DecodeError(e.to_string()))?;
Ok(decoded_bytes)
}
pub fn base64_to_string(b64s: &str) -> Result<String, KSMRError> {
let decoded_bytes = STANDARD
.decode(b64s)
.map_err(|_| KSMRError::DecodeError("Failed to decode Base64 string".to_string()))?;
let decoded_string = String::from_utf8(decoded_bytes)
.map_err(|_| KSMRError::DecodeError("Failed to convert bytes to UTF-8".to_string()))?;
Ok(decoded_string)
}
pub fn base64_to_string_lossy(b64s: &str) -> Result<String, KSMRError> {
let decoded_bytes = STANDARD
.decode(b64s)
.map_err(|_| KSMRError::DecodeError("Failed to decode Base64 string".to_string()))?;
let decoded_string = String::from_utf8_lossy(&decoded_bytes).to_string();
Ok(decoded_string)
}
pub fn string_to_bytes(s: &str) -> Vec<u8> {
s.as_bytes().to_vec() }
pub fn url_safe_str_to_bytes(s: &str) -> Result<Vec<u8>, crate::custom_error::KSMRError> {
URL_SAFE_NO_PAD
.decode(s)
.map_err(|e| KSMRError::DecodeError(e.to_string()))
}
pub fn url_safe_str_to_int(s: &str) -> Result<BigUint, KSMRError> {
let bytes_of_str = url_safe_str_to_bytes(s)?;
bytes_to_int(bytes_of_str.as_slice()).map_err(|e| KSMRError::DecodeError(e.to_string()))
}
pub fn generate_random_bytes(length: usize) -> Vec<u8> {
CryptoUtils::generate_random_bytes(length)
}
pub fn generate_uid_bytes() -> Vec<u8> {
let dash = [0xf8, 0x7f]; let mut uid_bytes: Vec<u8> = Vec::new();
for _ in 0..8 {
uid_bytes = generate_random_bytes(16);
if dash[0] & uid_bytes[0] != dash[0] {
break;
}
}
if dash[0] & uid_bytes[0] == dash[0] {
uid_bytes[0] &= dash[1];
}
uid_bytes
}
pub fn generate_uid() -> String {
let uid_bytes = generate_uid_bytes();
CryptoUtils::bytes_to_url_safe_str(&uid_bytes)
}
pub fn dict_to_json<T: Serialize>(dictionary: &T) -> serde_json::Result<String> {
serde_json::to_string_pretty(dictionary)
}
pub fn json_to_dict(json_str: &str) -> Option<HashMap<String, Value>> {
let return_value = serde_json::from_str(json_str).map_err(|err| {
warn!("JSON decode error: {}", err);
});
match return_value {
Ok(map) => Some(map),
Err(err) => {
warn!("JSON decode error: {:?}", err);
None
}
}
}
pub fn now_milliseconds() -> i64 {
Utc::now().timestamp_millis()
}
#[derive(Debug, Clone)]
pub struct TotpCode {
code: String,
time_left: u64, period: u64, }
impl TotpCode {
pub fn new(code: String, time_left: u64, period: u64) -> Self {
TotpCode {
code,
time_left,
period,
}
}
pub fn get_code(&self) -> &str {
&self.code
}
pub fn get_time_left(&self) -> u64 {
self.time_left
}
pub fn get_period(&self) -> u64 {
self.period
}
}
pub fn get_totp_code(url: &str) -> Result<TotpCode, KSMRError> {
let comp = Url::parse(url).map_err(|_| KSMRError::TOTPError("Invalid URL".to_string()))?;
if comp.scheme() != "otpauth" {
return Err(KSMRError::TOTPError("Not an otpauth URI".to_string()));
}
let mut secret = None;
let mut algorithm = "SHA1".to_string();
let mut digits = 6;
let mut period = 30;
let mut counter = 0;
let query_pairs = parse(comp.query().unwrap_or("").as_bytes());
for (key, value) in query_pairs {
match key.as_ref() {
"secret" => secret = Some(value.into_owned()),
"algorithm" => algorithm = value.into_owned().to_uppercase(),
"digits" => {
if let Ok(num) = value.parse::<u32>() {
if num > 0 && num < 10 {
digits = num;
} else {
return Err(KSMRError::TOTPError(
"TOTP Digits may only be 6, 7, or 8".to_string(),
));
}
}
}
"period" => {
if let Ok(num) = value.parse::<u32>() {
if num > 0 {
period = num;
}
}
}
"counter" => {
if let Ok(num) = value.parse::<u32>() {
if num > 0 {
counter = num;
}
}
}
_ => {}
}
}
let secret = secret
.ok_or(KSMRError::TOTPError(
"TOTP secret not found in URI".to_string(),
))?
.to_ascii_uppercase();
let decoded_key_option = BASE32.decode(secret.as_bytes());
let key = match decoded_key_option {
Ok(decoded_key) => decoded_key,
Err(err) => Err(KSMRError::DecodeError(format!(
"Invalid TOTP secret: {}",
err
)))?,
};
let tm_base = if counter > 0 {
counter
} else {
Utc::now().timestamp() as u32
};
let tm = tm_base / period;
let msg = (tm as u64).to_be_bytes();
let digest: Vec<u8> = match algorithm.as_str() {
"SHA1" => {
let mut hmac = Hmac::<Sha1>::new_from_slice(&key)
.map_err(|_| KSMRError::TOTPError("Failed to create HMAC".to_string()))?;
hmac.update(&msg);
hmac.finalize().into_bytes().to_vec()
}
"SHA256" => {
let mut hmac = Hmac::<Sha256>::new_from_slice(&key)
.map_err(|_| KSMRError::TOTPError("Failed to create HMAC".to_string()))?;
hmac.update(&msg);
hmac.finalize().into_bytes().to_vec()
}
"SHA512" => {
let mut hmac = Hmac::<Sha512>::new_from_slice(&key)
.map_err(|_| KSMRError::TOTPError("Failed to create HMAC".to_string()))?;
hmac.update(&msg);
hmac.finalize().into_bytes().to_vec()
}
_ => {
return Err(KSMRError::TOTPError(format!(
"Invalid algorithm: {}",
algorithm
)))
}
};
let offset = (digest.last().unwrap() & 0x0f) as usize;
let base = &digest[offset..offset + 4];
let code_int = ((base[0] & 0x7f) as u32) << 24
| (base[1] as u32) << 16
| (base[2] as u32) << 8
| (base[3] as u32);
let code = format!(
"{:0width$}",
code_int % 10u32.pow(digits),
width = digits as usize
);
let elapsed = tm_base % period; let ttl = period - elapsed;
Ok(TotpCode::new(code, ttl as u64, period as u64))
}
pub fn get_otp_url_from_value_obj(val: serde_json::Value) -> Result<String, KSMRError> {
let otp_value = match val.is_array() {
true => val.as_array().unwrap()[0][0].clone(),
false => {
return Err(KSMRError::RecordDataError(
"otpCode or otp field is not an array".to_string(),
))
}
};
let url_retrieved = match otp_value.is_string() {
true => otp_value.as_str().unwrap().to_string(),
false => {
return Err(KSMRError::RecordDataError(
"otpCode or otp field is not a string".to_string(),
))
}
};
Ok(url_retrieved)
}
pub fn random_sample(
sample_length: usize,
sample_string: &str,
) -> Result<String, Box<dyn std::error::Error>> {
if sample_length == 0 {
return Ok(String::new());
}
if sample_string.is_empty() {
return Err("sample_string must not be empty".into());
}
let mut rng = rand::thread_rng();
let mut sample = String::new();
for _ in 0..sample_length {
let char = sample_string
.chars()
.choose(&mut rng)
.ok_or("Failed to choose a character")?;
sample.push(char);
}
Ok(sample)
}
#[cfg(target_os = "windows")]
pub fn get_windows_user_sid_and_name<F>(command: Option<F>) -> (Option<String>, Option<String>)
where
F: Fn() -> Output,
{
let output = match command {
Some(comm) => comm(),
None => _default_command(),
};
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.lines().collect();
if let Some(last_line) = lines.last() {
let parts: Vec<&str> = last_line.split('\\').collect();
if let Some(username_sid) = parts.last() {
let username_sid_string = username_sid.to_string();
let split_parts: Vec<&str> = username_sid_string.split(" ").collect();
let username = split_parts[0].to_string();
let sid = split_parts[1].to_string();
return (Some(sid), Some(username));
}
}
} else {
eprintln!("Failed to execute 'whoami.exe'");
}
(None, None)
}
#[cfg(target_os = "windows")]
fn _default_command() -> Output {
Command::new("whoami.exe")
.arg("/user")
.output()
.expect("Failed to execute whoami.exe")
}
#[cfg(not(target_os = "windows"))]
fn _default_command() -> Output {
use std::os::unix::process::ExitStatusExt;
Output {
status: std::process::ExitStatus::from_raw(1),
stdout: Vec::new(),
stderr: Vec::new(),
}
}
pub fn set_config_mode(file: &str) -> Result<(), io::Error> {
if let Ok(skip_mode) = env::var("KSM_CONFIG_SKIP_MODE") {
if skip_mode.to_lowercase() == "true" {
return Ok(());
}
}
#[cfg(target_os = "windows")]
{
let sid = match get_windows_user_sid_and_name::<fn() -> Output>(None) {
(Some(sid), _) => sid,
_ => {
return Err(io::Error::new(
io::ErrorKind::Other,
"Failed to get user SID",
))
}
};
let commands = vec![
format!(r#"icacls "{}" /reset"#, file),
format!(r#"icacls "{}" /inheritance:r"#, file),
format!(r#"icacls "{}" /remove:g Everyone:F"#, file),
format!(r#"icacls "{}" /grant:r Administrators:F"#, file),
format!(r#"icacls "{}" /grant:r "{}:F""#, file, sid),
];
for command in commands {
let output = Command::new("cmd").args(&["/C", &command]).output()?;
match output.status.code() {
Some(2) => {
return Err(io::Error::new(
io::ErrorKind::NotFound,
format!("Cannot find configuration file {}", file),
))
}
Some(5) => {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("Access denied to configuration file {}", file),
))
}
Some(1332) => {
debug!("{} {}", "Failed to set some ACL permissions: {}", command);
continue; }
Some(_) if !output.status.success() => {
let message = format!(
"Could not change the ACL for file '{}'. Set the environmental variable 'KSM_CONFIG_SKIP_MODE' to 'TRUE' to skip setting the ACL mode.",
file
);
let stderr = String::from_utf8_lossy(&output.stderr);
let full_message = if !stderr.is_empty() {
format!("{}: {}", message, stderr.trim())
} else {
format!("{}.", message)
};
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
full_message,
));
}
_ => {}
}
}
}
#[cfg(not(target_os = "windows"))]
{
let permissions = fs::metadata(file)?.permissions();
let mut new_permissions = permissions;
new_permissions.set_mode(0o600);
fs::set_permissions(file, new_permissions)?;
}
Ok(())
}
#[cfg(target_os = "windows")]
fn _populate_windows_localized_admin_names_win32api() -> Result<Vec<String>, u32> {
use std::ffi::OsString; use std::os::windows::ffi::OsStringExt; use std::ptr;
use winapi::shared::winerror::{ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_PARAMETER};
use winapi::um::errhandlingapi::GetLastError;
use winapi::um::securitybaseapi::CreateWellKnownSid;
use winapi::um::winbase::LookupAccountSidW;
#[allow(non_camel_case_types)]
#[repr(u32)]
#[derive(Clone)]
pub enum WellKnownSidType {
WinLocalSystemSid = 22,
WinBuiltinAdministratorsSid = 26,
}
fn wide_to_string(wide: &[u16]) -> String {
let os_string = OsString::from_wide(wide);
os_string.to_string_lossy().into_owned()
}
fn get_account_name(sid_type: WellKnownSidType) -> Result<(String, String), u32> {
let mut sid_size = 256;
let mut sid = vec![0u8; sid_size as usize];
unsafe {
if CreateWellKnownSid(
sid_type.clone() as u32,
ptr::null_mut(),
sid.as_mut_ptr() as *mut _,
&mut sid_size,
) == 0
{
let error = GetLastError();
if error == ERROR_INSUFFICIENT_BUFFER || error == ERROR_INVALID_PARAMETER {
sid = vec![0u8; sid_size as usize];
if CreateWellKnownSid(
sid_type as u32,
ptr::null_mut(),
sid.as_mut_ptr() as *mut _,
&mut sid_size,
) == 0
{
return Err(GetLastError());
}
} else {
return Err(error);
}
}
let mut name_size = 0;
let mut domain_size = 0;
let mut sid_name_use = 0;
LookupAccountSidW(
ptr::null(),
sid.as_mut_ptr() as *mut _,
ptr::null_mut(),
&mut name_size,
ptr::null_mut(),
&mut domain_size,
&mut sid_name_use,
);
let error = GetLastError();
if error != ERROR_INSUFFICIENT_BUFFER {
return Err(error);
}
let mut name = vec![0u16; name_size as usize];
let mut domain = vec![0u16; domain_size as usize];
if LookupAccountSidW(
ptr::null(),
sid.as_mut_ptr() as *mut _,
name.as_mut_ptr(),
&mut name_size,
domain.as_mut_ptr(),
&mut domain_size,
&mut sid_name_use,
) == 0
{
return Err(GetLastError());
}
let domain_str = wide_to_string(&domain);
let name_str = wide_to_string(&name);
Ok((domain_str, name_str))
}
}
let mut localized_admins = Vec::new();
let mut admins = Vec::new();
if let Ok((_, name)) = get_account_name(WellKnownSidType::WinLocalSystemSid) {
admins.push(name);
}
if let Ok((_, name)) = get_account_name(WellKnownSidType::WinBuiltinAdministratorsSid) {
admins.push(name);
}
if !admins.is_empty() {
let mut cmd = String::from("echo.");
for admin in &admins {
cmd.push_str(&format!(" & echo {}", admin));
}
let output = std::process::Command::new("cmd")
.args(&["/C", &cmd])
.output()
.expect("Failed to execute command");
if output.status.success() {
let output_lines = output.stdout.split(|&b| b == b'\n');
for line in output_lines {
if !line.is_empty() {
localized_admins.push(String::from_utf8_lossy(line).trim().to_string());
}
}
}
}
Ok(localized_admins)
}
#[derive(Debug)]
pub enum ConfigError {
PermissionDenied(String),
FileNotFound(String),
GeneralError(String),
}
pub fn check_config_mode(file: &str) -> Result<bool, ConfigError> {
let skip_mode_check = env::var("KSM_CONFIG_SKIP_MODE")
.unwrap_or("FALSE".to_string())
.eq_ignore_ascii_case("TRUE");
if skip_mode_check {
return Ok(true);
}
#[cfg(target_os = "windows")]
return check_windows_permissions(file);
#[cfg(not(target_os = "windows"))]
return check_unix_permissions(file);
}
#[cfg(target_os = "windows")]
fn check_windows_permissions(file: &str) -> Result<bool, ConfigError> {
use std::process::Command;
let output = Command::new("icacls")
.arg(file)
.output()
.map_err(|e| ConfigError::GeneralError(format!("Error executing icacls: {}", e)))?;
if !output.status.success() {
return match output.status.code() {
Some(2) => Err(ConfigError::FileNotFound(file.to_string())),
Some(5) => Err(ConfigError::PermissionDenied(file.to_string())),
_ => Err(ConfigError::GeneralError(
"Unknown error in icacls".to_string(),
)),
};
}
if !is_file_accessible(file) {
return Err(ConfigError::PermissionDenied(format!(
"Access denied to {}",
file
)));
}
Ok(true)
}
#[cfg(not(target_os = "windows"))]
fn check_unix_permissions(file: &str) -> Result<bool, ConfigError> {
use std::path::Path;
let file_path = Path::new(file);
if !file_path.exists() {
return Err(ConfigError::FileNotFound(file.to_string()));
}
let metadata =
fs::metadata(file_path).map_err(|_| ConfigError::FileNotFound(file.to_string()))?;
if !is_file_accessible(file) {
return Err(ConfigError::PermissionDenied(file.to_string()));
}
let permissions = metadata.permissions().mode();
if permissions & 0o077 != 0 {
eprintln!(
"Warning: File permissions for {} are too open ({:o}). Consider setting to 0600.",
file, permissions
);
return Err(ConfigError::PermissionDenied(format!(
"File permissions too open for {}",
file
)));
}
Ok(true)
}
fn is_file_accessible(file: &str) -> bool {
File::open(file).is_ok()
}
#[derive(Debug)]
pub struct PasswordOptions {
length: usize,
lowercase: Option<i32>,
uppercase: Option<i32>,
digits: Option<i32>,
special_characters: Option<i32>,
special_characterset: String,
}
impl PasswordOptions {
pub fn new() -> Self {
PasswordOptions {
length: DEFAULT_PASSWORD_LENGTH,
lowercase: None,
uppercase: None,
digits: None,
special_characters: None,
special_characterset: String::from(SPECIAL_CHARACTERS),
}
}
pub fn length(mut self, length: usize) -> Self {
if length > 0 {
self.length = length;
} else {
self.length = 32
}
self
}
pub fn lowercase(mut self, count: i32) -> Self {
self.lowercase = Some(count);
self
}
pub fn uppercase(mut self, count: i32) -> Self {
self.uppercase = Some(count);
self
}
pub fn digits(mut self, count: i32) -> Self {
self.digits = Some(count);
self
}
pub fn special_characters(mut self, count: i32) -> Self {
self.special_characters = Some(count);
self
}
pub fn special_characterset(mut self, charset: String) -> Self {
self.special_characterset = charset;
self
}
}
impl Default for PasswordOptions {
fn default() -> Self {
Self::new()
}
}
pub fn generate_password_with_options(options: PasswordOptions) -> Result<String, KSMRError> {
let mut rng = thread_rng();
let all_explicit_exact = options.lowercase.is_some_and(|v| v <= 0)
&& options.uppercase.is_some_and(|v| v <= 0)
&& options.digits.is_some_and(|v| v <= 0)
&& options.special_characters.is_some_and(|v| v <= 0);
let has_any_minimum_or_none = options.lowercase.is_none()
|| options.lowercase.is_some_and(|v| v > 0)
|| options.uppercase.is_none()
|| options.uppercase.is_some_and(|v| v > 0)
|| options.digits.is_none()
|| options.digits.is_some_and(|v| v > 0)
|| options.special_characters.is_none()
|| options.special_characters.is_some_and(|v| v > 0);
let is_exact_mode = all_explicit_exact && !has_any_minimum_or_none;
let lowercase_count = options.lowercase.map_or(0, |v| v.abs());
let uppercase_count = options.uppercase.map_or(0, |v| v.abs());
let digits_count = options.digits.map_or(0, |v| v.abs());
let special_count = options.special_characters.map_or(0, |v| v.abs());
let total_specified = lowercase_count + uppercase_count + digits_count + special_count;
let target_length = if is_exact_mode {
total_specified as usize
} else {
options.length
};
if is_exact_mode && (options.length as i32) != total_specified {
warn!(
"Exact character counts specified (negative values) - password length will be {} instead of requested {}",
total_specified, options.length
);
}
let extra_count = if (target_length as i32) > total_specified {
target_length as i32 - total_specified
} else {
if !is_exact_mode && (options.length as i32) < total_specified {
warn!(
"Specified character counts ({}) exceed password length ({}) - no extra characters will be added",
total_specified, options.length
);
}
0
};
let has_positive_values = options.lowercase.is_some_and(|v| v > 0)
|| options.uppercase.is_some_and(|v| v > 0)
|| options.digits.is_some_and(|v| v > 0)
|| options.special_characters.is_some_and(|v| v > 0);
if has_positive_values && total_specified > (options.length as i32) {
return Err(KSMRError::PasswordCreationError(format!(
"The specified character counts ({}) exceed the total password length ({})!",
total_specified, options.length
)));
}
let mut extra_chars = String::new();
if options.lowercase.is_none() || options.lowercase.is_some_and(|v| v > 0) {
extra_chars.push_str("abcdefghijklmnopqrstuvwxyz");
}
if options.uppercase.is_none() || options.uppercase.is_some_and(|v| v > 0) {
extra_chars.push_str("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
}
if options.digits.is_none() || options.digits.is_some_and(|v| v > 0) {
extra_chars.push_str("0123456789");
}
if options.special_characters.is_none() || options.special_characters.is_some_and(|v| v > 0) {
extra_chars.push_str(&options.special_characterset);
}
if extra_chars.is_empty() {
extra_chars.push_str("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
extra_chars.push_str(SPECIAL_CHARACTERS);
}
let category_map = vec![
(lowercase_count as usize, "abcdefghijklmnopqrstuvwxyz"),
(uppercase_count as usize, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
(digits_count as usize, "0123456789"),
(special_count as usize, &options.special_characterset),
(extra_count.max(0) as usize, &extra_chars),
];
let mut password_list = Vec::new();
for (count, chars) in category_map {
let char_slice: Vec<char> = chars.chars().collect();
let mut repeated_chars = char_slice.iter().cycle(); for _ in 0..count {
if let Some(&sample) = repeated_chars.next() {
password_list.push(sample);
}
}
}
let mut remaining_length = target_length.saturating_sub(password_list.len());
while remaining_length > 0 {
let extra_char_slice: Vec<char> = extra_chars.chars().collect();
let additional_samples: Vec<char> = extra_char_slice
.choose_multiple(&mut rng, remaining_length)
.cloned()
.collect();
password_list.extend(additional_samples);
remaining_length = target_length.saturating_sub(password_list.len())
}
password_list.shuffle(&mut rng);
Ok(password_list.into_iter().collect())
}
pub fn generate_password() -> Result<String, KSMRError> {
let password_options_default = PasswordOptions::new();
generate_password_with_options(password_options_default)
}