├── .gitignore
├── .taurignore
├── Cargo.toml
├── LICENSE
├── README.md
├── Tauri.toml
├── assets
├── 128x128.png
├── 128x128@2x.png
├── 32x32.png
├── CascadiaMono.woff2
├── crypto-architecture.png
├── icon.icns
├── icon.ico
├── icon.svg
└── login-page.png
├── build.rs
├── package.json
├── src
├── cryptography.rs
├── database.rs
├── error.rs
├── logs.rs
└── main.rs
├── tsconfig.json
├── ui
├── AddPage.tsx
├── LoginPage.tsx
├── SignUpPage.tsx
├── StartPage.tsx
├── backend.tsx
├── index.html
├── main.tsx
└── utils.tsx
└── vite.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | target/
3 | Cargo.lock
4 |
5 | # These are backup files generated by rustfmt
6 | **/*.rs.bk
7 |
8 | # vite build dir
9 | build/
10 |
11 | # js
12 | node_modules/
13 | package-lock.json
14 |
--------------------------------------------------------------------------------
/.taurignore:
--------------------------------------------------------------------------------
1 | ui
2 | node_modules
3 | README.md
4 | .gitignore
5 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "pw-manager"
3 | version = "0.1.0"
4 | authors = ["Axel Lindeberg"]
5 | default-run = "pw-manager"
6 | edition = "2021"
7 |
8 | [dependencies]
9 | tauri = { version = "1.1", features = ["window-start-dragging"] }
10 | serde = { version = "1.0", features = ["derive"] }
11 | serde_json = "1.0"
12 | openssl = "0.10"
13 | once_cell = "1.13"
14 | arboard = "2.1"
15 | fern = "0.6"
16 | log = "0.4"
17 | chrono = "0.4"
18 |
19 | [dev-dependencies]
20 | tempfile = "3"
21 |
22 | [build-dependencies]
23 | tauri-build = { version = "1.1", features = ["config-toml"] }
24 |
25 | [features]
26 | default = [ "custom-protocol" ]
27 | custom-protocol = [ "tauri/custom-protocol" ]
28 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Axel Lindeberg
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tauri-pw-manager (working title)
2 | A desktop password manager using Tauri, with a backend in Rust and a frontend in Typescript and React.
3 |
4 | 
5 |
6 | ## Cryptography architecture
7 | 
8 |
9 | ## usage
10 | Assumes you have the [Rust toolchain](https://rustup.rs/) and [npm](https://www.npmjs.com/) installed.
11 |
12 | ```bash
13 | npm run setup # install dependencies
14 | cargo tauri dev # run the app in development mode
15 | cargo tauri build # build a release version of the app
16 | ```
17 |
--------------------------------------------------------------------------------
/Tauri.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | beforeDevCommand = "npm run --silent dev"
3 | devPath = "http://127.0.0.1:3000"
4 | beforeBuildCommand = "npm run --silent build"
5 | distDir = "./ui/build"
6 |
7 | [package]
8 | productName = "pw-manager"
9 | version = "0.1.0"
10 |
11 | [tauri.allowlist.window]
12 | startDragging = true
13 |
14 | [[tauri.windows]]
15 | title = "tauri pw manager"
16 | width = 800
17 | height = 600
18 | decorations = false
19 | transparent = true
20 |
21 | [tauri.bundle]
22 | identifier = "tauri.pw.manager"
23 | targets = "all"
24 | icon = [
25 | "assets/32x32.png",
26 | "assets/128x128.png",
27 | "assets/128x128@2x.png",
28 | "assets/icon.icns",
29 | "assets/icon.ico"
30 | ]
31 |
--------------------------------------------------------------------------------
/assets/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxlLind/tauri-pw-manager/763d20ee6c70add66778228de2d71744f16236f3/assets/128x128.png
--------------------------------------------------------------------------------
/assets/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxlLind/tauri-pw-manager/763d20ee6c70add66778228de2d71744f16236f3/assets/128x128@2x.png
--------------------------------------------------------------------------------
/assets/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxlLind/tauri-pw-manager/763d20ee6c70add66778228de2d71744f16236f3/assets/32x32.png
--------------------------------------------------------------------------------
/assets/CascadiaMono.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxlLind/tauri-pw-manager/763d20ee6c70add66778228de2d71744f16236f3/assets/CascadiaMono.woff2
--------------------------------------------------------------------------------
/assets/crypto-architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxlLind/tauri-pw-manager/763d20ee6c70add66778228de2d71744f16236f3/assets/crypto-architecture.png
--------------------------------------------------------------------------------
/assets/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxlLind/tauri-pw-manager/763d20ee6c70add66778228de2d71744f16236f3/assets/icon.icns
--------------------------------------------------------------------------------
/assets/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxlLind/tauri-pw-manager/763d20ee6c70add66778228de2d71744f16236f3/assets/icon.ico
--------------------------------------------------------------------------------
/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/assets/login-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxlLind/tauri-pw-manager/763d20ee6c70add66778228de2d71744f16236f3/assets/login-page.png
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "setup": "cargo install tauri-cli && npm install",
5 | "dev": "vite",
6 | "build": "tsc && vite build"
7 | },
8 | "dependencies": {
9 | "@emotion/react": "^11.9.3",
10 | "@emotion/styled": "^11.9.3",
11 | "@mui/icons-material": "^5.8.4",
12 | "@mui/material": "^5.8.7",
13 | "@tauri-apps/api": "^1.0.2",
14 | "react": "^18.0.0",
15 | "react-dom": "^18.0.0"
16 | },
17 | "devDependencies": {
18 | "@types/react": "^18.0.0",
19 | "@types/react-dom": "^18.0.0",
20 | "@vitejs/plugin-react": "^1.3.0",
21 | "typescript": "^4.6.3",
22 | "vite": "^2.9.9"
23 | },
24 | "volta": {
25 | "node": "18.5.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/cryptography.rs:
--------------------------------------------------------------------------------
1 | use std::marker::PhantomData;
2 | use openssl::error::ErrorStack;
3 | use openssl::hash::MessageDigest;
4 | use openssl::rand::rand_bytes;
5 | use openssl::symm::{Cipher, encrypt_aead, decrypt_aead, encrypt, decrypt};
6 | use serde::{Serialize, de::DeserializeOwned};
7 | use crate::error::Error;
8 |
9 | const AAD_MESSAGE: &[u8] = b"Tauri PW Manager v0.0.1";
10 |
11 | #[derive(Default)]
12 | pub struct EncryptedBlob {
13 | iv: [u8; 12],
14 | tag: [u8; 16],
15 | data: Vec,
16 | _t: PhantomData,
17 | }
18 |
19 | impl EncryptedBlob {
20 | pub fn encrypt(t: &T, key: &[u8]) -> Result {
21 | let iv = random_bytes::<12>();
22 | let mut tag = [0; 16];
23 | let serialized = serde_json::to_vec(&t)?;
24 | let data = encrypt_aead(Cipher::aes_256_gcm(), key, Some(&iv), AAD_MESSAGE, &serialized, &mut tag)?;
25 | Ok(Self { iv, tag, data, _t: PhantomData })
26 | }
27 |
28 | pub fn decrypt(&self, key: &[u8]) -> Result {
29 | let bytes = decrypt_aead(Cipher::aes_256_gcm(), key, Some(&self.iv), AAD_MESSAGE, &self.data, &self.tag)?;
30 | let t = serde_json::from_slice(&bytes)?;
31 | Ok(t)
32 | }
33 |
34 | pub fn from_bytes(bytes: &[u8]) -> Result {
35 | if bytes.len() < (12 + 16 + 1) {
36 | return Err(Error::InvalidDatabase);
37 | }
38 | Ok(Self {
39 | iv: bytes[0..12].try_into().unwrap(),
40 | tag: bytes[12..12+16].try_into().unwrap(),
41 | data: bytes[12+16..].to_vec(),
42 | _t: PhantomData,
43 | })
44 | }
45 |
46 | pub fn bytes(&self) -> impl Iterator- + '_ {
47 | self.iv.iter().chain(self.tag.iter()).chain(self.data.iter()).copied()
48 | }
49 | }
50 |
51 | pub fn random_bytes() -> [u8; SIZE] {
52 | let mut bytes = [0; SIZE];
53 | rand_bytes(&mut bytes).expect("failed to generate random bytes");
54 | bytes
55 | }
56 |
57 | pub fn pbkdf2_hmac(password: &[u8], salt: &[u8]) -> [u8; 32] {
58 | let mut key = [0; 32];
59 | openssl::pkcs5::pbkdf2_hmac(password, salt, 100_000, MessageDigest::sha256(), &mut key).expect("pbkdf2 should not fail");
60 | key
61 | }
62 |
63 | pub fn encrypt_key(master_key: &[u8], key: &[u8]) -> Result<([u8; 32], [u8; 16]), ErrorStack> {
64 | let nonce = random_bytes::<16>();
65 | let ciphertext = encrypt(Cipher::aes_256_ctr(), master_key, Some(&nonce), key)?;
66 | Ok((ciphertext.try_into().unwrap(), nonce))
67 | }
68 |
69 | pub fn decrypt_key(master_key: &[u8], encrypted_key: &[u8], nonce: &[u8]) -> Result<[u8; 32], ErrorStack> {
70 | let plaintext = decrypt(Cipher::aes_256_ctr(), master_key, Some(nonce), encrypted_key)?;
71 | Ok(plaintext.try_into().unwrap())
72 | }
73 |
74 | pub fn generate_password(alphabet: &[u8], len: usize) -> String {
75 | assert!(alphabet.len() < 256);
76 | let mod_ceil = alphabet.len().next_power_of_two();
77 | (0..len).map(|_| loop {
78 | let [b] = random_bytes::<1>();
79 | if let Some(&c) = alphabet.get(b as usize % mod_ceil) {
80 | return c as char;
81 | }
82 | }).collect()
83 | }
84 |
85 | #[cfg(test)]
86 | mod tests {
87 | use super::*;
88 |
89 | #[test]
90 | fn test_encrypt_decrypt_lob() {
91 | let data = "This is some serious data right here".to_string();
92 | let key = random_bytes::<32>();
93 | let blob1 = EncryptedBlob::encrypt(&data, &key).expect("this should encrypt");
94 | let blob2 = EncryptedBlob::encrypt(&data, &key).expect("this should encrypt");
95 |
96 | // encrypted with two random ivs
97 | assert_ne!(blob1.iv, blob2.iv);
98 | assert_ne!(blob1.data, blob2.data);
99 | assert_ne!(blob1.tag, blob2.tag);
100 |
101 | let decrypted_data1 = blob1.decrypt(&key).expect("this should decrypt");
102 | let decrypted_data2 = blob2.decrypt(&key).expect("this should decrypt");
103 | assert_eq!(decrypted_data1, data);
104 | assert_eq!(decrypted_data2, data);
105 |
106 | // a single bit flip should mean failure
107 | let mut blob = EncryptedBlob::encrypt(&data, &key).expect("this should encrypt");
108 | blob.data[4] += 1;
109 | assert!(blob.decrypt(&key).is_err());
110 | }
111 |
112 | #[test]
113 | fn test_blob_to_from_vec() {
114 | let bytes = random_bytes::<128>().to_vec();
115 | let blob = EncryptedBlob::>::from_bytes(&bytes).expect("should be convertable");
116 | assert_eq!(bytes, blob.bytes().collect::>());
117 |
118 | let too_few_bytes = [0; 28];
119 | assert!(EncryptedBlob::>::from_bytes(&too_few_bytes).is_err());
120 | }
121 |
122 | #[test]
123 | fn test_generate_password() {
124 | assert_eq!(generate_password(b"a", 5), "aaaaa");
125 |
126 | const ASCII_PRINTABLE: &[u8] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~";
127 | let pw = generate_password(ASCII_PRINTABLE, 2000);
128 | for c in pw.bytes() {
129 | assert!(ASCII_PRINTABLE.contains(&c));
130 | }
131 | assert_eq!(pw.len(), 2000);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/src/database.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use serde::{Serialize, Deserialize};
3 |
4 | #[derive(Default, Hash, Serialize, Deserialize, Clone)]
5 | pub struct Credential {
6 | pub username: String,
7 | pub password: String,
8 | }
9 |
10 | #[derive(Default, Serialize, Deserialize)]
11 | pub struct CredentialsDatabase {
12 | username: String,
13 | credentials: HashMap
14 | }
15 |
16 | impl CredentialsDatabase {
17 | pub fn new(username: String) -> Self {
18 | Self { username, ..Self::default() }
19 | }
20 |
21 | pub fn username(&self) -> &str { &self.username }
22 |
23 | pub fn add(&mut self, name: String, username: String, password: String) {
24 | self.credentials.insert(name, Credential { username, password });
25 | }
26 |
27 | pub fn remove(&mut self, name: &str) -> bool {
28 | self.credentials.remove(name).is_some()
29 | }
30 |
31 | pub fn entry(&self, name: &str) -> Option<&Credential> {
32 | self.credentials.get(name)
33 | }
34 |
35 | pub fn entries(&self) -> impl Iterator
- {
36 | self.credentials.iter()
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use serde::{Serialize, ser::SerializeMap};
2 | use crate::logs;
3 |
4 | #[derive(Debug, Clone, Copy)]
5 | pub enum Error {
6 | InvalidCredentials,
7 | InvalidDatabase,
8 | InvalidParameter,
9 | UsernameTaken,
10 | Unexpected,
11 | }
12 |
13 | impl Error {
14 | fn message(&self) -> &'static str {
15 | match self {
16 | Self::InvalidCredentials => "invalid credentials",
17 | Self::InvalidDatabase => "corrupt key database",
18 | Self::InvalidParameter => "invalid parameter",
19 | Self::UsernameTaken => "username already registered",
20 | Self::Unexpected => "unexpected error occurred"
21 | }
22 | }
23 |
24 | fn key(&self) -> &'static str {
25 | match self {
26 | Self::InvalidCredentials => "invalid_credentials",
27 | Self::InvalidDatabase => "invalid_database",
28 | Self::InvalidParameter => "invalid_parameter",
29 | Self::UsernameTaken => "username_taken",
30 | Self::Unexpected => "unexpected",
31 | }
32 | }
33 | }
34 |
35 | impl From for Error {
36 | fn from(e: T) -> Self {
37 | logs::error!("Unexpected error", e);
38 | Self::Unexpected
39 | }
40 | }
41 |
42 | impl Serialize for Error {
43 | fn serialize(&self, serializer: S) -> Result {
44 | let mut json_err = serializer.serialize_map(Some(2))?;
45 | json_err.serialize_entry("key", self.key())?;
46 | json_err.serialize_entry("error", self.message())?;
47 | json_err.end()
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/logs.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | use std::path::Path;
3 | use log::LevelFilter;
4 | use chrono::{Duration, Local, NaiveDate};
5 | use crate::error::Error;
6 |
7 | pub fn initialize(logs_folder: &Path) -> Result<(), Error> {
8 | if !logs_folder.exists() {
9 | fs::create_dir(&logs_folder)?;
10 | }
11 | let log_file = Local::now().format("%Y-%m-%d.log").to_string();
12 | fern::Dispatch::new()
13 | .format(|out, message, record| {
14 | out.finish(format_args!(
15 | "[{}][{}] {}",
16 | Local::now().format("%Y-%m-%d %H:%M:%S"),
17 | record.level(),
18 | message,
19 | ));
20 | })
21 | .level(if cfg!(debug_assertions) { log::LevelFilter::Debug } else { LevelFilter::Info })
22 | .chain(std::io::stdout())
23 | .chain(fern::log_file(logs_folder.join(log_file))?)
24 | .apply()?;
25 | Ok(())
26 | }
27 |
28 | pub fn remove_old(log_folder: &Path) -> Result<(), Error> {
29 | let deletion_point = Local::now().naive_local() - Duration::days(3);
30 | for e in fs::read_dir(log_folder)? {
31 | let path = e?.path();
32 | if !path.is_file() || path.extension().map_or(false, |e| e != "log") {
33 | continue;
34 | }
35 | let file_stem = path.file_stem().unwrap().to_string_lossy();
36 | let log_time = match NaiveDate::parse_from_str(&file_stem, "%Y-%m-%d") {
37 | Ok(d) => d.and_hms_opt(0, 0, 0).unwrap(),
38 | Err(_) => continue,
39 | };
40 | if log_time < deletion_point {
41 | info!("removing old log file", file=path.file_name().unwrap().to_string_lossy());
42 | fs::remove_file(path)?;
43 | }
44 | }
45 | Ok(())
46 | }
47 |
48 | #[doc(hidden)]
49 | macro_rules! __log_arguments {
50 | // Parse a `key=value` argument, into `"key={}", value`
51 | ($level:expr, $prefix:literal, $fmt:expr $(, $args:expr)* ; $k:ident=$v:expr, $($tokens:tt)*) => {
52 | $crate::logs::__log_arguments!($level, ", ", concat!($fmt, $prefix, stringify!($k), "={}") $(, $args)*, $v ; $($tokens)*)
53 | };
54 | // Parse a `key=?value` argument, into `"key={:?}", value`
55 | ($level:expr, $prefix:literal, $fmt:expr $(, $args:expr)* ; $k:ident=?$v:expr, $($tokens:tt)*) => {
56 | $crate::logs::__log_arguments!($level, ", ", concat!($fmt, $prefix, stringify!($k), "={:?}") $(, $args)*, $v ; $($tokens)*)
57 | };
58 | // Parse a `key` argument, into `"key={}", key`
59 | ($level:expr, $prefix:literal, $fmt:expr $(, $args:expr)* ; $k:ident, $($tokens:tt)*) => {
60 | $crate::logs::__log_arguments!($level, ", ", concat!($fmt, $prefix, stringify!($k), "={}") $(, $args)*, $k ; $($tokens)*)
61 | };
62 | // Output the final log expression
63 | ($level:expr, $_:literal, $fmt:expr $(, $args:expr)* ; $(,)?) => {
64 | ::log::log!($level, $fmt, $($args),*)
65 | };
66 | }
67 |
68 | #[doc(hidden)]
69 | macro_rules! __log_impl {
70 | ($level:expr, $msg:literal, $($tokens:tt)*) => {
71 | $crate::logs::__log_arguments!($level, ": ", $msg ; $($tokens)*)
72 | };
73 | }
74 |
75 | macro_rules! debug { ($($tokens:tt)+) => { $crate::logs::__log_impl!(::log::Level::Debug, $($tokens)+,) } }
76 | macro_rules! info { ($($tokens:tt)+) => { $crate::logs::__log_impl!(::log::Level::Info, $($tokens)+,) } }
77 | macro_rules! _warn { ($($tokens:tt)+) => { $crate::logs::__log_impl!(::log::Level::Warn, $($tokens)+,) } }
78 | macro_rules! error { ($($tokens:tt)+) => { $crate::logs::__log_impl!(::log::Level::Error, $($tokens)+,) } }
79 |
80 | pub(crate) use { __log_impl, __log_arguments };
81 | #[allow(unused)] pub(crate) use { debug, info, _warn as warn, error };
82 |
83 | #[cfg(test)]
84 | mod tests {
85 | use super::*;
86 | use tempfile::tempdir;
87 |
88 | #[test]
89 | fn test_remove_old_deletion_dates() -> std::io::Result<()> {
90 | let dir = tempdir()?;
91 | let now = Local::now().naive_local();
92 | for days_old in 0..5 {
93 | let log = dir.path().join((now - Duration::days(days_old)).format("%Y-%m-%d.log").to_string());
94 | fs::write(&log, "unittest")?;
95 | }
96 | assert_eq!(fs::read_dir(dir.path())?.count(), 5);
97 | remove_old(dir.path()).expect("remove_old should not fail");
98 | assert_eq!(fs::read_dir(dir.path())?.count(), 3);
99 | Ok(())
100 | }
101 |
102 | #[test]
103 | fn test_remove_old_invalid_files() -> std::io::Result<()> {
104 | let dir = tempdir()?;
105 | fs::write(dir.path().join("random_name.log"), "unittest")?;
106 | fs::write(dir.path().join("2000-99-99.log"), "unittest")?;
107 | fs::write(dir.path().join("1984-01-01.txt"), "unittest")?;
108 | fs::write(dir.path().join("1984-01-01.txt.log"), "unittest")?;
109 | fs::create_dir(dir.path().join("nested_dir"))?;
110 | assert_eq!(fs::read_dir(dir.path())?.count(), 5);
111 | remove_old(dir.path()).expect("remove_old should not fail");
112 | assert_eq!(fs::read_dir(dir.path())?.count(), 5);
113 | Ok(())
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | #![forbid(unsafe_code)]
2 | #![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
3 | mod database;
4 | mod cryptography;
5 | mod error;
6 | mod logs;
7 |
8 | use std::fs;
9 | use std::path::PathBuf;
10 | use std::sync::Mutex;
11 | use database::Credential;
12 | use once_cell::sync::Lazy;
13 | use arboard::Clipboard;
14 | use tauri::State;
15 | use crate::cryptography::EncryptedBlob;
16 | use crate::database::CredentialsDatabase;
17 | use crate::error::Error;
18 |
19 | pub static APP_FOLDER: Lazy = Lazy::new(|| {
20 | if cfg!(target_os = "windows") {
21 | let appdata = std::env::var("APPDATA").expect("$APPDATA not set!");
22 | [&appdata, "tauri-pw-manager"].iter().collect()
23 | } else {
24 | let home = std::env::var("HOME").expect("$HOME not set!");
25 | [&home, ".config", "tauri-pw-manager"].iter().collect()
26 | }
27 | });
28 |
29 | #[derive(Default)]
30 | struct UserSession {
31 | file: PathBuf,
32 | nonce: [u8; 16],
33 | encrypted_key: [u8; 32],
34 | key: [u8; 32],
35 | db: CredentialsDatabase,
36 | }
37 |
38 | fn save_database(session: &UserSession) -> Result<(), Error> {
39 | let encrypted_blob = EncryptedBlob::encrypt(&session.db, &session.key)?;
40 | let file_content = session.nonce.iter()
41 | .copied()
42 | .chain(session.encrypted_key)
43 | .chain(encrypted_blob.bytes())
44 | .collect::>();
45 | fs::write(&session.file, &file_content)?;
46 | Ok(())
47 | }
48 |
49 | #[tauri::command]
50 | fn window_close(window: tauri::Window) -> Result<(), Error> {
51 | logs::debug!("Closing window");
52 | window.close().map_err(|_| Error::Unexpected)
53 | }
54 |
55 | #[tauri::command]
56 | fn window_minimize(window: tauri::Window) -> Result<(), Error> {
57 | logs::debug!("Minimizing window");
58 | window.minimize().map_err(|_| Error::Unexpected)
59 | }
60 |
61 | #[tauri::command]
62 | fn window_toggle_fullscreen(window: tauri::Window) -> Result<(), Error> {
63 | let fullscreen = window.is_fullscreen()?;
64 | logs::debug!("Toggling window fullscreen", fullscreen);
65 | window.set_fullscreen(!fullscreen)?;
66 | Ok(())
67 | }
68 |
69 | #[tauri::command]
70 | fn copy_to_clipboard(name: String, thing: String, session_mutex: State<'_, Mutex