aboutsummaryrefslogtreecommitdiff
path: root/rust/src/implementation/passwordmaker
diff options
context:
space:
mode:
Diffstat (limited to 'rust/src/implementation/passwordmaker')
-rw-r--r--rust/src/implementation/passwordmaker/emittingsender.rs17
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs31
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs131
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/message_parsing.rs102
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/mod.rs49
-rw-r--r--rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs46
-rw-r--r--rust/src/implementation/passwordmaker/mod.rs305
-rw-r--r--rust/src/implementation/passwordmaker/settings.rs159
-rw-r--r--rust/src/implementation/passwordmaker/thread_messages.rs41
9 files changed, 881 insertions, 0 deletions
diff --git a/rust/src/implementation/passwordmaker/emittingsender.rs b/rust/src/implementation/passwordmaker/emittingsender.rs
new file mode 100644
index 0000000..1cf58fc
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/emittingsender.rs
@@ -0,0 +1,17 @@
+use std::sync::mpsc::{channel,Sender,Receiver,SendError};
+#[derive(Clone)]
+pub(crate) struct EmittingSender<MT, E : Fn()> {
+ sender : Sender<MT>,
+ emit : E,
+}
+
+impl <MT, E : Fn()> EmittingSender<MT, E>{
+ pub(crate) fn emitting_channel(emit : E) -> (Self, Receiver<MT>) {
+ let (sender,r) = channel();
+ (EmittingSender { sender, emit }, r)
+ }
+ pub(crate) fn send(&self, t: MT) -> Result<(), SendError<MT>> {
+ //first send, then notify.
+ self.sender.send(t).map(|_| (self.emit)())
+ }
+}
diff --git a/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs b/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs
new file mode 100644
index 0000000..85fbc39
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/hashers/mod.rs
@@ -0,0 +1,31 @@
+mod qt_hash;
+use passwordmaker_rs::{Hasher, HasherList};
+use ripemd::Digest;
+
+
+pub(super) use qt_hash::{QtMd4, QtMd5, QtSha1, QtSha256};
+pub(super) struct Ripemd160;
+
+impl Hasher for Ripemd160{
+ type Output = [u8;20];
+
+ fn hash(input : &[u8]) -> Self::Output {
+ let hash = ripemd::Ripemd160::digest(input);
+ hash.into()
+ }
+}
+
+pub(super) struct PassFishHashers;
+impl HasherList for PassFishHashers {
+ type MD4 = QtMd4;
+ type MD5 = QtMd5;
+ type SHA1 = QtSha1;
+ type SHA256 = QtSha256;
+ type RIPEMD160 = Ripemd160;
+}
+
+impl passwordmaker_rs::Md4 for QtMd4 {}
+impl passwordmaker_rs::Md5 for QtMd5 {}
+impl passwordmaker_rs::Sha1 for QtSha1 {}
+impl passwordmaker_rs::Sha256 for QtSha256 {}
+impl passwordmaker_rs::Ripemd160 for Ripemd160 {}
diff --git a/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs b/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs
new file mode 100644
index 0000000..64d4e80
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/hashers/qt_hash.rs
@@ -0,0 +1,131 @@
+use std::borrow::{BorrowMut, Borrow};
+use passwordmaker_rs::Hasher;
+use libc::size_t;
+
+pub(crate) struct Md4;
+pub(crate) struct Md5;
+pub(crate) struct Sha1;
+pub(crate) struct Sha256;
+
+pub(crate) struct QHasher<T>(T);
+
+pub(crate) type QtMd4 = QHasher<Md4>;
+pub(crate) type QtMd5 = QHasher<Md5>;
+pub(crate) type QtSha1 = QHasher<Sha1>;
+pub(crate) type QtSha256 = QHasher<Sha256>;
+
+impl<T> Hasher for QHasher<T> where T:QtHasher {
+ type Output = T::Output;
+
+ fn hash(input : &[u8]) -> Self::Output {
+ let mut result = T::Output::default();
+ let required_bytes = result.borrow().len();
+ let computed_bytes = unsafe{
+ pwm_qhash(
+ T::QT_ALGO_NUMBER,
+ input.as_ptr(),
+ input.len() as size_t,
+ result.borrow_mut().as_mut_ptr(),
+ required_bytes)
+ };
+ assert_eq!(computed_bytes, required_bytes); //no point to forward this to caller. It's a code bug, plain and simple.
+ result
+ }
+}
+
+pub(crate) trait QtHasher{
+ type Output : Default + BorrowMut<[u8]>;
+ const QT_ALGO_NUMBER : size_t;
+}
+
+impl QtHasher for Md4 {
+ type Output = [u8;16];
+ const QT_ALGO_NUMBER : size_t = 0;
+}
+
+impl QtHasher for Md5 {
+ type Output = [u8;16];
+ const QT_ALGO_NUMBER : size_t = 1;
+}
+
+impl QtHasher for Sha1 {
+ type Output = [u8;20];
+ const QT_ALGO_NUMBER : size_t = 2;
+}
+
+impl QtHasher for Sha256 {
+ type Output = [u8;32];
+ const QT_ALGO_NUMBER : size_t = 4;
+}
+
+#[cfg(test)]
+use rust_testhelper::pwm_qhash;
+#[cfg(not(test))]
+extern "C"{
+ fn pwm_qhash(algorithm : size_t, input : *const u8, input_length : size_t, output : *mut u8, output_capacity : size_t) -> size_t;
+}
+
+/// Those tests are testing the integration of Qt's QCryptographicHash function. They are functional tests, NOT unit tests.
+#[cfg(test)]
+mod qt_hash_tests{
+ use super::*;
+ fn get_simple_string_as_bytes() -> &'static [u8] {
+ "I am a simple string and I like simple things. I like dancing in the rain, I like eating hamburgers, and I like you.".as_bytes()
+ }
+ fn get_complex_string_as_bytes() -> &'static [u8] {
+ "I am a complex string and do complex stuff. I like 🕺 in the 🌧️. I like eating 🍔, and I ❤️ you.".as_bytes()
+ }
+ #[test]
+ fn md4_simple_string_test(){
+ let hash = QtMd4::hash(get_simple_string_as_bytes());
+ let expected = vec![0x14, 0x1f, 0x1f, 0x1d, 0xef, 0x4a, 0x0d, 0x15, 0x1d, 0xb5, 0x5f, 0x7c, 0xb8, 0x96, 0xca, 0x99];
+ assert_eq!(hash.borrow(), expected);
+ }
+ #[test]
+ fn md4_complex_string_test(){
+ let hash = QtMd4::hash(get_complex_string_as_bytes());
+ let expected = vec![0xc1, 0xfb, 0xe3, 0x4d, 0x72, 0x75, 0xb3, 0xa5, 0x3a, 0xc3, 0x45, 0xcf, 0x90, 0x34, 0x81, 0xf7];
+ assert_eq!(hash.borrow(), expected);
+ }
+
+ #[test]
+ fn md5_simple_string_test(){
+ let hash = QtMd5::hash(get_simple_string_as_bytes());
+ let expected = vec![0x4b, 0x2e, 0x18, 0x22, 0x45, 0x43, 0xf3, 0x96, 0xee, 0x79, 0x53, 0x18, 0x90, 0x1b, 0xb9, 0x7f];
+ assert_eq!(hash.borrow(), expected);
+ }
+ #[test]
+ fn md5_complex_string_test(){
+ let hash = QtMd5::hash(get_complex_string_as_bytes());
+ let expected = vec![0x70, 0x31, 0x35, 0xd9, 0x38, 0x55, 0x1d, 0x2a, 0xae, 0xfa, 0xd9, 0x38, 0x07, 0x91, 0x11, 0xfe ];
+ assert_eq!(hash.borrow(), expected);
+ }
+
+ #[test]
+ fn sha1_simple_string_test(){
+ let hash = QtSha1::hash(get_simple_string_as_bytes());
+ let expected = vec![0xa1, 0x0a, 0x15, 0x18, 0x99, 0x29, 0x9d, 0xc7, 0xa6, 0x48, 0x36, 0x11, 0x44, 0xb3, 0x94, 0x09, 0x87, 0x3a, 0x39, 0xf3];
+ assert_eq!(hash.borrow(), expected);
+ }
+ #[test]
+ fn sha1_complex_string_test(){
+ let hash = QtSha1::hash(get_complex_string_as_bytes());
+ let expected = vec![0x30, 0x79, 0xcd, 0xbc, 0x66, 0x09, 0xad, 0x24, 0x99, 0x44, 0xe5, 0x52, 0x25, 0xdf, 0xb4, 0x68, 0xfd, 0x5f, 0xb9, 0x8f ];
+ assert_eq!(hash.borrow(), expected);
+ }
+
+ #[test]
+ fn sha256_simple_string_test(){
+ let hash = QtSha256::hash(get_simple_string_as_bytes());
+ let expected = vec![0xd4, 0xaf, 0x13, 0x6a, 0x87, 0x62, 0x12, 0xf1, 0x93, 0x7d, 0xd1, 0x71, 0xab, 0xa1, 0xfa, 0x3e, 0x3b, 0x8e, 0xc5, 0x68,
+ 0xed, 0x42, 0x46, 0x9d, 0xf0, 0x9b, 0xd0, 0xd8, 0xd8, 0x39, 0x09, 0x93];
+ assert_eq!(hash.borrow(), expected);
+ }
+ #[test]
+ fn sha256_complex_string_test(){
+ let hash = QtSha256::hash(get_complex_string_as_bytes());
+ let expected = vec![0x01, 0x3d, 0x93, 0x17, 0x45, 0x18, 0x29, 0x41, 0x6a, 0x09, 0xb5, 0x65, 0x1b, 0x81, 0x32, 0x88, 0xce, 0x83, 0xad, 0x92,
+ 0x04, 0x0f, 0x24, 0x13, 0x57, 0x8d, 0xd1, 0xa5, 0xe8, 0x3a, 0x73, 0xaa ];
+ assert_eq!(hash.borrow(), expected);
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs b/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs
new file mode 100644
index 0000000..4cda572
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/message_parsing.rs
@@ -0,0 +1,102 @@
+use std::sync::mpsc::{Receiver, RecvError};
+use super::super::thread_messages::UiToHelper;
+
+pub(super) fn receive_and_get_newest_or_important_command(receiver : &Receiver<UiToHelper>) -> Result<NewestMostImportantCommand, RecvError> {
+ let first_message = receive_and_log_error(receiver)?;
+ Ok(get_newest_or_important_command(first_message, receiver.try_iter()))
+}
+
+pub(super) struct NewestMostImportantCommand(pub UiToHelper);
+
+trait GetNewerOrMoreImportantCommand {
+ type Output;
+ fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output;
+ fn convert_first_command(self) -> Self::Output;
+}
+
+impl GetNewerOrMoreImportantCommand for UiToHelper {
+ type Output = NewestMostImportantCommand;
+
+ fn convert_first_command(self) -> Self::Output{
+ NewestMostImportantCommand(self)
+ }
+ fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output {
+ match old.0 {
+ UiToHelper::Shutdown => old,
+ UiToHelper::GeneratePassword(_) => NewestMostImportantCommand(new),
+ }
+ }
+}
+
+
+fn get_newest_or_important_command<I, It >(first_message: I, other_messages: It) -> I::Output
+ where I: GetNewerOrMoreImportantCommand,
+ It: Iterator<Item=I>
+{
+ other_messages.fold(first_message.convert_first_command(), I::get_newer_or_more_important_command)
+}
+
+fn receive_and_log_error(receiver : &Receiver<UiToHelper>) -> Result<UiToHelper, RecvError> {
+ match receiver.recv() {
+ Ok(x) => Ok(x),
+ e => {
+ eprintln!("Connection to UI Thread closed unexpectedly.");
+ e
+ }
+ }
+}
+
+#[cfg(test)]
+mod message_parsing_tests {
+ use super::*;
+
+ #[derive(PartialEq, Debug)]
+ enum TestPrioritizedEnum {
+ Lowest(usize),
+ Highest(usize),
+ Medium(usize)
+ }
+ impl TestPrioritizedEnum{
+ fn get_prio(&self) -> usize{
+ match self {
+ TestPrioritizedEnum::Lowest(_) => 0,
+ TestPrioritizedEnum::Highest(_) => 2,
+ TestPrioritizedEnum::Medium(_) => 1,
+ }
+ }
+ }
+
+ struct TestPrioritizedEnumPrioResult(TestPrioritizedEnum);
+ impl GetNewerOrMoreImportantCommand for TestPrioritizedEnum {
+ type Output = TestPrioritizedEnumPrioResult;
+
+ fn get_newer_or_more_important_command(old: Self::Output, new: Self) -> Self::Output {
+ if old.0.get_prio() > new.get_prio() { old } else { TestPrioritizedEnumPrioResult(new) }
+ }
+
+ fn convert_first_command(self) -> Self::Output {
+ TestPrioritizedEnumPrioResult(self)
+ }
+ }
+
+ #[test]
+ fn test_get_newest_or_important_command_test(){
+ let input1 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Lowest(2)];
+ let expected1 = TestPrioritizedEnum::Lowest(2);
+ let input2 = vec![TestPrioritizedEnum::Medium(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Lowest(2)];
+ let expected2 = TestPrioritizedEnum::Medium(0);
+ let input3 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Medium(1), TestPrioritizedEnum::Lowest(2)];
+ let expected3 = TestPrioritizedEnum::Medium(1);
+ let input4 = vec![TestPrioritizedEnum::Medium(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Medium(2)];
+ let expected4 = TestPrioritizedEnum::Medium(2);
+ let input5 = vec![TestPrioritizedEnum::Lowest(0), TestPrioritizedEnum::Lowest(1), TestPrioritizedEnum::Highest(2), TestPrioritizedEnum::Medium(3)];
+ let expected5 = TestPrioritizedEnum::Highest(2);
+ let data = vec![(input1, expected1), (input2, expected2), (input3, expected3), (input4, expected4), (input5, expected5)];
+ for (input, expected) in data {
+ let mut it = input.into_iter();
+ let first = it.next().unwrap();
+ let result = get_newest_or_important_command(first,it);
+ assert_eq!(result.0, expected);
+ }
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/passwordmaker/helperthread/mod.rs b/rust/src/implementation/passwordmaker/helperthread/mod.rs
new file mode 100644
index 0000000..7e3b587
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/mod.rs
@@ -0,0 +1,49 @@
+mod message_parsing;
+mod hashers;
+mod profile_to_domain;
+use std::sync::mpsc::{Receiver, SendError};
+use passwordmaker_rs::PasswordMaker;
+use profile_to_domain::{convert_hash_algorithm, convert_leet};
+use super::emittingsender::EmittingSender;
+use super::thread_messages::{HelperToUi, UiToHelper};
+use message_parsing::receive_and_get_newest_or_important_command;
+use hashers::PassFishHashers;
+
+pub(super) fn run<T: Fn()>(to_ui : &EmittingSender<HelperToUi, T>, from_ui : &Receiver<UiToHelper>) -> Result<(), SendError<HelperToUi>>{
+ println!("Helper Thread starting up.");
+
+ while let Ok(m) = receive_and_get_newest_or_important_command(from_ui)
+ {
+ //m is a product type, because we gather the latest command of each type. Easiest to deal with those would be using a group of ifs.
+ //for now the number of alternatives is rather small though, so let's match instead.
+ match m.0 {
+ UiToHelper::Shutdown => break,
+ UiToHelper::GeneratePassword(task) => {
+
+ to_ui.send(HelperToUi::GenerationStarted)?;
+ let hash_algorithm = convert_hash_algorithm(&task.generation_settings.hash_algorithm);
+ let use_leet = convert_leet(&task.generation_settings.leet);
+ let characters = &task.generation_settings.characters;
+ let username = &task.generation_settings.username;
+ let modifier = &task.generation_settings.modifier;
+ let password_length = task.generation_settings.password_length as usize;
+ let suffix = &task.generation_settings.suffix;
+ let prefix = &task.generation_settings.prefix;
+ type Pwm<'a> = PasswordMaker<'a, PassFishHashers>;
+ let pwm = Pwm::new(hash_algorithm, use_leet, characters, username, modifier, password_length, prefix, suffix);
+ let input = task.input;
+ let master_password = task.master_password;
+ let pwm = pwm.map_err(Into::into);
+ let password =
+ pwm.and_then(|pwm| pwm.generate(input, master_password).map_err(Into::into));
+
+ match password {
+ Ok(password) => to_ui.send(HelperToUi::Generated{password})?,
+ Err(error) => to_ui.send(HelperToUi::GenerationFailed{ error })?,
+ }
+ }
+ }
+ }
+ println!("Helper Thread shutting down.");
+ Ok(())
+}
diff --git a/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs b/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs
new file mode 100644
index 0000000..152ba8f
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/helperthread/profile_to_domain.rs
@@ -0,0 +1,46 @@
+/// This whole module may look dumb, but that might change, once the public interface of passwordmaker_rs starts to deviate from the saved data.
+
+use passwordmaker_rs::{HashAlgorithm,UseLeetWhenGenerating,LeetLevel};
+
+pub(super) fn convert_hash_algorithm(stored_algo : &crate::implementation::profiles::HashAlgorithm) -> HashAlgorithm {
+ use crate::implementation::profiles::HashAlgorithm as PHashAlgorithm;
+ match stored_algo {
+ PHashAlgorithm::Md4 => HashAlgorithm::Md4,
+ PHashAlgorithm::HmacMd4 => HashAlgorithm::HmacMd4,
+ PHashAlgorithm::Md5 => HashAlgorithm::Md5,
+ PHashAlgorithm::Md5Version06 => HashAlgorithm::Md5Version06,
+ PHashAlgorithm::HmacMd5 => HashAlgorithm::HmacMd5,
+ PHashAlgorithm::HmacMd5Version06 => HashAlgorithm::HmacMd5Version06,
+ PHashAlgorithm::Sha1 => HashAlgorithm::Sha1,
+ PHashAlgorithm::HmacSha1 => HashAlgorithm::HmacSha1,
+ PHashAlgorithm::Sha256 => HashAlgorithm::Sha256,
+ PHashAlgorithm::HmacSha256 => HashAlgorithm::HmacSha256,
+ PHashAlgorithm::Ripemd160 => HashAlgorithm::Ripemd160,
+ PHashAlgorithm::HmacRipemd160 => HashAlgorithm::HmacRipemd160,
+ }
+}
+
+pub(super) fn convert_leet(stored_leet : &crate::implementation::profiles::UseLeetWhenGenerating) -> UseLeetWhenGenerating {
+ use crate::implementation::profiles::UseLeetWhenGenerating as PUseLeetWhenGenerating;
+ match stored_leet {
+ PUseLeetWhenGenerating::NotAtAll => UseLeetWhenGenerating::NotAtAll,
+ PUseLeetWhenGenerating::Before { level } => UseLeetWhenGenerating::Before { level: convert_leet_level(level) },
+ PUseLeetWhenGenerating::After { level } => UseLeetWhenGenerating::After { level: convert_leet_level(level) },
+ PUseLeetWhenGenerating::BeforeAndAfter { level } => UseLeetWhenGenerating::BeforeAndAfter { level: convert_leet_level(level) },
+ }
+}
+
+fn convert_leet_level(stored_level : &crate::implementation::profiles::LeetLevel) -> LeetLevel {
+ use crate::implementation::profiles::LeetLevel as PLeetLevel;
+ match stored_level {
+ PLeetLevel::One => LeetLevel::One,
+ PLeetLevel::Two => LeetLevel::Two,
+ PLeetLevel::Three => LeetLevel::Three,
+ PLeetLevel::Four => LeetLevel::Four,
+ PLeetLevel::Five => LeetLevel::Five,
+ PLeetLevel::Six => LeetLevel::Six,
+ PLeetLevel::Seven => LeetLevel::Seven,
+ PLeetLevel::Eight => LeetLevel::Eight,
+ PLeetLevel::Nine => LeetLevel::Nine,
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/passwordmaker/mod.rs b/rust/src/implementation/passwordmaker/mod.rs
new file mode 100644
index 0000000..af0516c
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/mod.rs
@@ -0,0 +1,305 @@
+mod emittingsender;
+mod thread_messages;
+mod settings;
+mod helperthread;
+
+pub use self::settings::Settings;
+use passwordmaker_rs::{UrlParsing, ProtocolUsageMode, SettingsError, GenerationError};
+use crate::interface::ProfilesTrait;
+
+use std::cell::RefCell;
+use std::convert::TryFrom;
+use std::error::Error;
+use std::fmt::Display;
+use std::sync::mpsc::{Receiver, Sender, channel};
+use crate::implementation::pwm_macros::EnumVariantCount;
+
+use mockall_double::double;
+
+#[double]
+use crate::interface::PasswordMakerEmitter;
+use crate::interface::PasswordMakerTrait;
+use super::Profiles;
+use self::emittingsender::EmittingSender;
+use self::thread_messages::{GeneratePasswordTask, HelperToUi, UiToHelper, GenerationIssue};
+
+#[derive(EnumVariantCount, Clone, Copy)]
+enum GeneratorState{
+ MissingTextToUse,
+ MissingMasterPassword,
+ CharsetError,
+ GenerationCompleted,
+ Busy
+}
+
+pub struct PasswordMaker {
+ emit : PasswordMakerEmitter,
+ profiles : Profiles,
+ settings : Settings,
+ url : RefCell<String>,
+ used_text : RefCell<String>,
+ master_password : RefCell<String>,
+ generated_password : RefCell<String>,
+ generator_state : RefCell<GeneratorState>,
+ from_helper : Receiver<HelperToUi>,
+ to_helper : Sender<UiToHelper>,
+ helper_thread : Option<std::thread::JoinHandle<()>>
+}
+
+#[derive(Debug)]
+struct GeneratorStateFromIntError;
+impl Display for GeneratorStateFromIntError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f,"Invalid value for GeneratorStateFromIntError.")
+ }
+}
+impl Error for GeneratorStateFromIntError{}
+
+impl TryFrom<u8> for GeneratorState {
+ type Error = GeneratorStateFromIntError;
+ fn try_from(value: u8) -> Result<Self, Self::Error> {
+ match value {
+ 0 => Ok(GeneratorState::GenerationCompleted),
+ 1 => Ok(GeneratorState::Busy),
+ 2 => Ok(GeneratorState::MissingTextToUse),
+ 3 => Ok(GeneratorState::MissingMasterPassword),
+ 4 => Ok(GeneratorState::CharsetError),
+ 5..=255 => Err(GeneratorStateFromIntError)
+ }
+ }
+}
+impl From<GeneratorState> for u8 {
+ fn from(s : GeneratorState) -> Self {
+ match s {
+ GeneratorState::MissingTextToUse => 2,
+ GeneratorState::GenerationCompleted => 0,
+ GeneratorState::Busy => 1,
+ GeneratorState::MissingMasterPassword => 3,
+ GeneratorState::CharsetError => 4,
+ }
+ }
+}
+
+impl PasswordMakerTrait for PasswordMaker {
+ fn new(emit : PasswordMakerEmitter, profiles : Profiles, settings : Settings) -> Self {
+ let stored_settings_data = settings.load();
+ if let Ok(x) = &stored_settings_data {
+ #[allow(clippy::cast_possible_truncation)]
+ profiles.set_current_profile(x.current_profile_index as u32);
+ }
+ let settings = if let Ok(x) = stored_settings_data { x.settings } else { settings };
+
+ let emit_clone = emit.clone();
+ let (to_ui,from_helper) = EmittingSender::emitting_channel(
+ move || {emit_clone.i_say_sexy_things_to_myself_while_im_dancing_changed();}
+ );
+ let (to_helper, from_ui) = channel();
+ let helper_thread = Some(std::thread::spawn(move || helperthread::run(&to_ui, &from_ui).expect("UI Thread hung up.") ));
+ PasswordMaker{emit, profiles, settings, from_helper, to_helper, helper_thread,
+ url: RefCell::new(String::new()), used_text: RefCell::new(String::new()), master_password: RefCell::new(String::new()), generated_password: RefCell::new(String::new()), generator_state: RefCell::new(GeneratorState::MissingTextToUse) }
+ }
+ fn emit(&self) -> &PasswordMakerEmitter {
+ &self.emit
+ }
+ fn profiles(&self) -> &Profiles {
+ &self.profiles
+ }
+ fn settings(&self) -> &Settings {
+ &self.settings
+ }
+ fn store_settings(&self) -> bool {
+ self.settings.store(self.profiles.current_profile() as usize).is_ok()
+ }
+ fn i_say_sexy_things_to_myself_while_im_dancing(&self) -> bool {
+ unreachable!()
+ }
+ fn set_i_say_sexy_things_to_myself_while_im_dancing(&self, _ : bool) {
+ if let Ok(m) = self.from_helper.try_recv() {
+ self.handle_message_from_helper(m);
+ } else {
+ println!("Spurious wakeup from helper thread. No message queued. Please investigate.");
+ }
+ }
+
+ fn generated_password<F>(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.generated_password.borrow());
+ }
+
+ fn master_password<F>(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.master_password.borrow());
+ }
+
+ fn set_master_password(&self, value: String) {
+ let different = {
+ let mut b = self.master_password.borrow_mut();
+ let different = *b != value;
+ if different {*b = value;}
+ different
+ };
+ if different {
+ self.update_generated_password();
+ self.emit().master_password_changed();
+ }
+ }
+
+ fn url<F>(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.url.borrow());
+ }
+
+ fn set_url(&self, value: String) {
+ let different = {
+ let mut b = self.url.borrow_mut();
+ let different = *b != value;
+ if different { *b = value; }
+ different
+ };
+ if different {
+ self.update_used_text_from_url();
+ self.emit().url_changed();
+ }
+ }
+
+ fn used_text<F>(&self, setter: F) where F: FnOnce(&str) {
+ setter(&*self.used_text.borrow());
+ }
+
+ fn set_used_text(&self, value: String) {
+ let different = {
+ let mut b = self.used_text.borrow_mut();
+ let different = *b != value;
+ if different { *b = value; }
+ different
+ };
+ if different {
+ self.update_generated_password();
+ self.emit().used_text_changed();
+ }
+ }
+
+ fn generator_state(&self) -> u8 {
+ (*self.generator_state.borrow()).into()
+ }
+
+ fn profile_changed(&self) {
+ self.update_used_text_from_url();
+ }
+}
+
+impl Drop for PasswordMaker
+{
+ fn drop(&mut self) {
+ if self.to_helper.send(UiToHelper::Shutdown).is_err() {
+ eprintln!("Failed to tell worker thread to quit. Might need to kill the process.");
+ }
+ if let Some(j) = self.helper_thread.take() {
+ if j.join().is_err() {
+ eprintln!("Helper Thread crashed. This should not happen, so please report a bug.");
+ }
+ } else {
+ eprintln!("Somehow the information about the helper thread got lost, so we can't wait for it to quit. Might need to kill the process.");
+ }
+ }
+}
+
+impl PasswordMaker{
+ fn handle_message_from_helper(&self, message : HelperToUi){
+ //unless everything is terribly wrong, we are in the UI thread here.
+ match message {
+ HelperToUi::Generated{ password } => {
+ //println!("Password generated.");
+ {
+ *self.generator_state.borrow_mut() = GeneratorState::GenerationCompleted;
+ *self.generated_password.borrow_mut() = password;
+ }
+ self.emit().generator_state_changed();
+ self.emit().generated_password_changed();
+ }
+ HelperToUi::GenerationFailed { error } => {
+ //println!("No password generated due to missing input.");
+ {
+ *self.generator_state.borrow_mut() = match error {
+ GenerationIssue::Settings(SettingsError::InsufficientCharset) => GeneratorState::CharsetError,
+ GenerationIssue::Input(GenerationError::MissingMasterPassword) => GeneratorState::MissingMasterPassword,
+ GenerationIssue::Input(GenerationError::MissingTextToUse) => GeneratorState::MissingTextToUse,
+ };
+ self.generated_password.borrow_mut().clear();
+ }
+ self.emit().generator_state_changed();
+ self.emit().generated_password_changed();
+ },
+ HelperToUi::GenerationStarted => {
+ //println!("Setting password generator state as busy.");
+ {
+ *self.generator_state.borrow_mut() = GeneratorState::Busy;
+ self.generated_password.borrow_mut().clear();
+ }
+ self.emit().generator_state_changed();
+ self.emit().generated_password_changed();
+ },
+ }
+ }
+ fn update_used_text_from_url(&self) {
+ let used_text = {
+ self.profiles.do_with_current_url_parsing_settings(
+ |settings| {
+ //have to convert saved data to corresponding runtime data.
+ let use_protocol = match (settings.use_protocol, settings.use_undefined_as_protocol_fallback) {
+ (true, false) => ProtocolUsageMode::Used,
+ (true, true) => ProtocolUsageMode::UsedWithUndefinedIfEmpty,
+ (false, _) => ProtocolUsageMode::Ignored,
+ };
+ let url_parsing = UrlParsing::new(
+ use_protocol,
+ settings.use_userinfo,
+ settings.use_subdomains,
+ settings.use_domain,
+ settings.use_port_path);
+ url_parsing.parse(&self.url.borrow())
+ }
+ )
+ };
+ { *self.used_text.borrow_mut() = used_text.unwrap_or_else(|e| e.to_string()); }
+ self.emit().used_text_changed();
+ self.update_generated_password(); //intentionally unconditional.
+ }
+ fn update_generated_password(&self) {
+ let generation_settings = self.profiles.get_copy_current_generation_settings();
+ match generation_settings {
+ Ok(generation_settings) => {
+ self.to_helper.send(
+ UiToHelper::GeneratePassword(
+ GeneratePasswordTask{
+ input: self.used_text.borrow().clone(),
+ master_password: self.master_password.borrow().clone(),
+ generation_settings }
+ )
+ ).expect("Helper thread no longer listening. Unrecoverable.");
+ }
+ Err(error) => {
+ {*self.generated_password.borrow_mut() = error.to_string();}
+ {*self.generator_state.borrow_mut() = GeneratorState::GenerationCompleted;}
+ self.emit().generator_state_changed();
+ self.emit().generated_password_changed();
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod passwordmaker_tests {
+ use std::convert::TryInto;
+
+ use super::*;
+ #[test]
+ fn generator_state_reciprocity() {
+ for i in 0..(GeneratorState::variant_count() as u8) {
+ let g : GeneratorState = i.try_into().unwrap();
+ let j : u8 = g.into();
+ assert_eq!(j,i);
+ }
+ for i in (GeneratorState::variant_count() as u8)..=255 {
+ let g : Result<GeneratorState,_> = i.try_into();
+ assert!(g.is_err());
+ }
+ }
+}
diff --git a/rust/src/implementation/passwordmaker/settings.rs b/rust/src/implementation/passwordmaker/settings.rs
new file mode 100644
index 0000000..487ffea
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/settings.rs
@@ -0,0 +1,159 @@
+use crate::interface::SettingsTrait;
+use crate::implementation::{LoadError, StoreError};
+
+use serde::{Serialize, Deserialize};
+use std::default::Default;
+use std::cell::RefCell;
+
+use mockall_double::double;
+
+#[double]
+use crate::interface::SettingsEmitter;
+
+#[derive(Serialize, Deserialize)]
+struct SettingsSaveData {
+ current_profile_index : usize,
+ settings : SettingsData
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct SettingsData{
+ clear_generated_password_seconds : Option<u32>,
+ clear_master_password_seconds : Option<u32>,
+ hide_generated_password : bool
+}
+
+impl Default for SettingsData {
+ fn default() -> Self {
+ SettingsData {
+ clear_generated_password_seconds : Some(60),
+ clear_master_password_seconds : Some(300),
+ hide_generated_password : false
+ }
+ }
+}
+
+pub struct Settings {
+ emit : SettingsEmitter,
+ data : RefCell<SettingsData>
+}
+
+pub struct SettingLoadResult {
+ pub settings : Settings,
+ pub current_profile_index : usize
+}
+
+impl Settings {
+ /// Loads the data from disk and merges them with the current Settings object. Only use during startup, as it does NOT emit.
+ pub(super) fn load(&self) -> Result<SettingLoadResult, LoadError>{
+ super::super::get_config_folder()
+ .map(|p| p.join("settings"))
+ .ok_or(LoadError::Xdg)
+ .and_then(|p| std::fs::read_to_string(p).map_err(Into::into))
+ .and_then(|s| toml::from_str(&s).map_err(Into::into))
+ .map(|sd| self.merge_with_loaded_data_no_emit(sd))
+ }
+ pub(super) fn store(&self, current_profile_index : usize) -> Result<(), StoreError>{
+ toml::to_string(&SettingsSaveData{current_profile_index, settings : self.data.borrow().clone()})
+ .map_err(Into::into)
+ .and_then(|s| Self::write_serialized_settings_data(&s))
+ }
+
+ ///Private. Should only be used during load().
+ fn merge_with_loaded_data_no_emit(&self, save_data: SettingsSaveData) -> SettingLoadResult {
+ SettingLoadResult {
+ settings : Settings { emit : self.emit.clone(), data : RefCell::new(save_data.settings) },
+ current_profile_index : save_data.current_profile_index
+ }
+ }
+ fn write_serialized_settings_data(data : &str) -> Result<(), StoreError> {
+ super::super::get_config_folder()
+ .ok_or(StoreError::Xdg)
+ .and_then(|p| std::fs::create_dir_all(p.clone()).map_err(Into::into).map(|()| p))
+ .map(|p| p.join("settings"))
+ .and_then(|f| std::fs::write(f, data).map_err(Into::into))
+ }
+}
+
+impl SettingsTrait for Settings {
+ fn new(emit: SettingsEmitter) -> Self {
+ Settings {emit, data : RefCell::new(SettingsData::default())}
+ }
+ fn emit(&self) -> &SettingsEmitter{
+ &self.emit
+ }
+ fn clear_generated_password_seconds(&self) -> Option<u32> {
+ self.data.borrow().clear_generated_password_seconds
+ }
+ fn set_clear_generated_password_seconds(&self, value: Option<u32>) {
+ let changed = self.data.borrow().clear_generated_password_seconds != value;
+ self.data.borrow_mut().clear_generated_password_seconds = value;
+ if changed {
+ self.emit().clear_generated_password_seconds_changed();
+ }
+ }
+ fn clear_master_password_seconds(&self) -> Option<u32> {
+ self.data.borrow().clear_master_password_seconds
+ }
+ fn set_clear_master_password_seconds(&self, value: Option<u32>) {
+ let changed = self.data.borrow().clear_master_password_seconds != value;
+ self.data.borrow_mut().clear_master_password_seconds = value;
+ if changed {
+ self.emit().clear_master_password_seconds_changed();
+ }
+ }
+
+ fn hide_generated_password(&self) -> bool {
+ self.data.borrow().hide_generated_password
+ }
+
+ fn set_hide_generated_password(&self, value: bool) {
+ let changed = {
+ let mut v = self.data.borrow_mut();
+ let changed = v.hide_generated_password != value;
+ v.hide_generated_password = value;
+ changed
+ };
+ if changed {
+ self.emit().hide_generated_password_changed();
+ }
+ }
+}
+
+#[cfg(test)]
+mod settings_test{
+ use super::*;
+
+ #[test]
+ fn merge_with_loaded_data_test(){
+ let mut emit = SettingsEmitter::new();
+ emit.expect_clone().return_once(||{
+ let mut e2 = SettingsEmitter::new();
+ e2.expect_clone().never();
+ e2.expect_clear_generated_password_seconds_changed().never();
+ e2.expect_clear_master_password_seconds_changed().never();
+ e2.expect_hide_generated_password_changed().never();
+ e2
+ }).once();
+ emit.expect_clear_generated_password_seconds_changed().never();
+ emit.expect_clear_master_password_seconds_changed().never();
+ emit.expect_hide_generated_password_changed().never();
+ let old_settings = Settings {
+ emit,
+ data: RefCell::new(SettingsData::default()),
+ };
+ let save_data = SettingsSaveData{
+ current_profile_index: 2,
+ settings: SettingsData {
+ clear_generated_password_seconds: Some(600),
+ clear_master_password_seconds: None,
+ hide_generated_password: true
+ },
+ };
+ let result = old_settings.merge_with_loaded_data_no_emit(save_data);
+ assert_eq!(result.current_profile_index, 2);
+ assert_eq!(result.settings.data.borrow().clear_generated_password_seconds, Some(600));
+ assert_eq!(result.settings.data.borrow().clear_master_password_seconds, None);
+ assert_eq!(result.settings.data.borrow().hide_generated_password, true);
+ }
+} \ No newline at end of file
diff --git a/rust/src/implementation/passwordmaker/thread_messages.rs b/rust/src/implementation/passwordmaker/thread_messages.rs
new file mode 100644
index 0000000..bba3dfa
--- /dev/null
+++ b/rust/src/implementation/passwordmaker/thread_messages.rs
@@ -0,0 +1,41 @@
+use std::sync::Arc;
+use passwordmaker_rs::{GenerationError,SettingsError};
+use crate::implementation::profiles::GenerationSettings;
+
+pub(super) enum HelperToUi {
+ Generated {
+ password : String,
+ },
+ GenerationFailed {
+ error : GenerationIssue
+ },
+ GenerationStarted
+}
+
+pub(super) struct GeneratePasswordTask{
+ pub(crate) input : String,
+ pub(crate) master_password : String,
+ pub(crate) generation_settings : Arc<GenerationSettings>
+}
+
+pub(super) enum UiToHelper {
+ Shutdown,
+ GeneratePassword(GeneratePasswordTask),
+}
+
+pub(super) enum GenerationIssue {
+ Settings(SettingsError),
+ Input(GenerationError),
+}
+
+impl From<SettingsError> for GenerationIssue {
+ fn from(s: SettingsError) -> Self {
+ GenerationIssue::Settings(s)
+ }
+}
+
+impl From<GenerationError> for GenerationIssue {
+ fn from(g: GenerationError) -> Self {
+ GenerationIssue::Input(g)
+ }
+} \ No newline at end of file