diff --git a/README.md b/README.md index 76e1e8c..966a5d0 100755 --- a/README.md +++ b/README.md @@ -106,6 +106,7 @@ Read more at: "] license = "Apache-2.0" @@ -8,8 +10,6 @@ repository = "https://github.com/otentikapp/clients" edition = "2021" rust-version = "1.63" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [build-dependencies] tauri-build = { version = "1.0.4", features = [] } @@ -17,12 +17,14 @@ tauri-build = { version = "1.0.4", features = [] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.0.5", features = ["api-all", "macos-private-api", "system-tray", "updater"] } +# uuid = { version = "1.1", features = ["v4", "fast-rng", "serde", "js"] } libreauth = { version = "^0.15", features = ["key", "oath", "pass"] } -binascii = { version = "^0.1", features = ["encode", "decode"] } +sha2 = { version = "0.10", default-features = false } cocoa = "0.24" magic-crypt = "^3.1" bcrypt = "^0.13" -easy-hasher = "^2.2" +sysinfo = "0.26" +machine-uid = "0.2" [dependencies.tauri-plugin-store] git = "https://github.com/tauri-apps/tauri-plugin-store" diff --git a/apps/desktop/src-tauri/rustfmt.toml b/apps/desktop/src-tauri/rustfmt.toml index d962cda..58c073a 100644 --- a/apps/desktop/src-tauri/rustfmt.toml +++ b/apps/desktop/src-tauri/rustfmt.toml @@ -1,4 +1,4 @@ -max_width = 100 +max_width = 120 hard_tabs = false tab_spaces = 2 newline_style = "Auto" diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index d583e40..785476d 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,80 +1,60 @@ -#![cfg_attr( - all(not(debug_assertions), target_os = "windows"), - windows_subsystem = "windows" -)] +// Copyright 2022 Aris Ripandi +// SPDX-License-Identifier: Apache-2.0 + +#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")] +#![allow(clippy::nonstandard_macro_braces)] // Clippy bug: https://github.com/rust-lang/rust-clippy/issues/7422 #[cfg(target_os = "linux")] use std::path::PathBuf; use cocoa::appkit::{NSWindow, NSWindowStyleMask, NSWindowTitleVisibility}; +// use tauri::api::dialog::ask; use tauri::{Manager, Runtime, SystemTray, SystemTrayEvent, Window}; use tauri_plugin_store::PluginBuilder; mod menu; mod otp_generator; mod security; +mod sys_utils; + +#[tauri::command] +fn exit_app(handle: tauri::AppHandle) { + handle.exit(0); +} fn main() { let system_tray = SystemTray::new(); - let app = tauri::Builder::default(); - app + tauri::Builder::default() .plugin(PluginBuilder::default().build()) .menu(menu::menu()) .on_menu_event(|event| match event.menu_item_id() { "quit" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } "close" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } "export" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } "import" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } "lock_vault" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } "new_item" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } "signout" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } "sync_vault" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } "update_check" => { - let _ = event - .window() - .emit("menu-event", event.menu_item_id()) - .unwrap(); + let _ = event.window().emit("app-event", event.menu_item_id()).unwrap(); } _ => {} }) @@ -116,14 +96,32 @@ fn main() { }) .invoke_handler(tauri::generate_handler![ otp_generator::generate_totp, + sys_utils::get_device_info, + security::generate_udevice_id, security::encrypt_str, security::decrypt_str, security::create_hash, security::verify_hash, - security::md5_hash, + security::generate_passphrase, + exit_app, ]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + .build(tauri::generate_context!()) + .expect("error while running tauri application") + .run(|app_handle, event| match event { + tauri::RunEvent::ExitRequested { .. } => { + // api.prevent_exit(); + app_handle.exit(0); + // TODO: call exit method from the frontend! + // let window = app_handle.get_window("main").unwrap(); + // ask(Some(&window), "Tauri", "Is Tauri awesome?", |answer| { + // if answer { + // app_handle.exit(0); + // } + // }); + // window.emit("app-event", "quit").unwrap(); + } + _ => {} + }) } pub trait WindowExt { @@ -138,10 +136,7 @@ impl WindowExt for Window { let id = self.ns_window().unwrap() as cocoa::base::id; NSWindow::setTitlebarAppearsTransparent_(id, cocoa::base::YES); let mut style_mask = id.styleMask(); - style_mask.set( - NSWindowStyleMask::NSFullSizeContentViewWindowMask, - title_transparent, - ); + style_mask.set(NSWindowStyleMask::NSFullSizeContentViewWindowMask, title_transparent); if remove_tool_bar { style_mask.remove( diff --git a/apps/desktop/src-tauri/src/menu.rs b/apps/desktop/src-tauri/src/menu.rs index 0ba4f0d..eaa002c 100644 --- a/apps/desktop/src-tauri/src/menu.rs +++ b/apps/desktop/src-tauri/src/menu.rs @@ -9,10 +9,10 @@ pub(crate) fn menu() -> Menu { // App Menu (macOS) // --------------------------------------------------------------------------------------------- let about_menu = AboutMetadata::new() - .version(String::from("0.6.0")) + .version(String::from("0.7.0")) .authors(vec![String::from("Aris Ripandi")]) .comments(String::from("Open Source two factor authenticator")) - .copyright(String::from("Apache-2.0 License")) + .copyright(String::from("Copyright © Aris Ripandi")) .license(String::from("Apache-2.0 License")) .website(String::from("https://otentik.app/authenticator")) .website_label(String::from("Homepage")); diff --git a/apps/desktop/src-tauri/src/otp_generator.rs b/apps/desktop/src-tauri/src/otp_generator.rs index b811020..4f0712e 100644 --- a/apps/desktop/src-tauri/src/otp_generator.rs +++ b/apps/desktop/src-tauri/src/otp_generator.rs @@ -1,30 +1,25 @@ use libreauth::{hash::HashFunction, oath::TOTPBuilder}; #[tauri::command] -pub(crate) async fn generate_totp( - secret: String, - period: u32, - digits: usize, - algorithm: String, -) -> String { - let hash_function = match algorithm.to_uppercase().as_str() { - "SHA1" => HashFunction::Sha1, - "SHA256" => HashFunction::Sha256, - "SH512" => HashFunction::Sha512, - // Set default to SHA1 - _ => HashFunction::Sha1, - }; +pub(crate) async fn generate_totp(secret: String, period: u32, digits: usize, algorithm: String) -> String { + let hash_function = match algorithm.to_uppercase().as_str() { + "SHA1" => HashFunction::Sha1, + "SHA256" => HashFunction::Sha256, + "SH512" => HashFunction::Sha512, + // Set default to SHA1 + _ => HashFunction::Sha1, + }; - let totp = TOTPBuilder::new() - .base32_key(&secret) - .period(period) - .output_len(digits) - .hash_function(hash_function) - .finalize() - .unwrap(); + let totp = TOTPBuilder::new() + .base32_key(&secret) + .period(period) + .output_len(digits) + .hash_function(hash_function) + .finalize() + .unwrap(); - let token = totp.generate(); + let token = totp.generate(); - // println!("{}", token); - token.into() + // println!("{}", token); + token.into() } diff --git a/apps/desktop/src-tauri/src/security.rs b/apps/desktop/src-tauri/src/security.rs index 83cb258..7c4bbc0 100644 --- a/apps/desktop/src-tauri/src/security.rs +++ b/apps/desktop/src-tauri/src/security.rs @@ -1,39 +1,64 @@ extern crate bcrypt; -extern crate easy_hasher; +extern crate machine_uid; + +use tauri::command; use bcrypt::{hash, verify, DEFAULT_COST}; -use easy_hasher::easy_hasher::*; use magic_crypt::{new_magic_crypt, MagicCryptTrait}; +use sha2::{Digest, Sha256}; -#[tauri::command] +#[command] pub(crate) async fn encrypt_str(plain_str: String, passphrase: String) -> String { - let mc = new_magic_crypt!(passphrase, 256); - let encrypted = mc.encrypt_str_to_base64(plain_str); - encrypted.into() + let mc = new_magic_crypt!(passphrase, 256); + let encrypted = mc.encrypt_str_to_base64(plain_str); + // println!("Encrypted string: {:?}", encrypted); + encrypted.into() } -#[tauri::command] +#[command] pub(crate) async fn decrypt_str(encrypted_str: String, passphrase: String) -> String { - let mc = new_magic_crypt!(passphrase, 256); - let decrypted = mc.decrypt_base64_to_string(&encrypted_str).unwrap(); - decrypted.into() + let mc = new_magic_crypt!(passphrase, 256); + let decrypted = mc.decrypt_base64_to_string(&encrypted_str).unwrap(); + decrypted.into() } -#[tauri::command] +#[command] pub(crate) async fn create_hash(plaintext: String) -> String { - let hashed = hash(plaintext, DEFAULT_COST).unwrap(); - hashed.into() + let hashed = hash(plaintext, DEFAULT_COST).unwrap(); + hashed.into() } -#[tauri::command] +#[command] pub(crate) async fn verify_hash(plaintext: String, hashed_str: String) -> bool { - let valid = verify(plaintext, &hashed_str).unwrap(); - valid.into() + let valid = verify(plaintext, &hashed_str).unwrap(); + valid.into() +} + +#[command] +pub(crate) async fn generate_passphrase(user_id: String, password: String) -> String { + let uid = user_id.replace("-", "").to_uppercase(); + let hash = Sha256::new().chain_update(&uid).chain_update(&password).finalize(); + let encoded = format!("{:x}", hash); + // println!("SHA2-encoded hash: {:?}", encoded); + encoded.into() } -#[tauri::command] -pub(crate) async fn md5_hash(str: String) -> String { - let hash = md5(&str); - let string_hash = hash.to_hex_string(); - string_hash.into() +#[command] +pub(crate) async fn generate_udevice_id(uid: String) -> String { + let device_uuid: String = machine_uid::get().unwrap(); + + // Get trimmed device_uuid + let a_trimmed = device_uuid.replace("-", ""); + let a_slice = &a_trimmed[..12]; + let _a = a_slice.to_string(); + + // Get trimmed external uuid + let b_trimmed = uid.replace("-", ""); + let b_slice = &b_trimmed[..12]; + let _b = b_slice.to_string(); + + let new_id = _a + &_b; + let result = new_id.to_uppercase(); + + result.into() } diff --git a/apps/desktop/src-tauri/src/sys_utils.rs b/apps/desktop/src-tauri/src/sys_utils.rs new file mode 100644 index 0000000..cfb9356 --- /dev/null +++ b/apps/desktop/src-tauri/src/sys_utils.rs @@ -0,0 +1,41 @@ +extern crate machine_uid; + +use sysinfo::{System, SystemExt}; +use tauri::command; + +#[derive(serde::Serialize)] +pub struct DeviceInfo { + device_uuid: String, + os_platform: String, + os_version: String, + host_name: String, +} + +#[command] +pub(crate) async fn get_device_info() -> Result { + // Initiate `sysinfo` instance. We use "new_all" to ensure that + // all list of components already filled. + let mut sys = System::new_all(); + + // Update all information of our `System` struct. + sys.refresh_all(); + + // System information: + let sys_device_uuid: String = machine_uid::get().unwrap(); + let sys_os_platform: String = sys.name().unwrap().to_string(); + let sys_os_version: String = sys.os_version().unwrap().to_string(); + let sys_host_name: String = sys.host_name().unwrap().to_string(); + + // Display system information: + // println!("System name : {:?}", sys_os_platform); + // println!("System OS version : {:?}", sys_os_version); + // println!("System host name : {:?}", sys_host_name); + // println!("System machine id : {:?}", sys_device_uuid); + + Ok(DeviceInfo { + device_uuid: sys_device_uuid.into(), + os_platform: sys_os_platform.into(), + os_version: sys_os_version.into(), + host_name: sys_host_name.into(), + }) +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index c2c07f1..866d311 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Authenticator", - "version": "0.6.0" + "version": "0.7.0" }, "tauri": { "allowlist": { @@ -22,6 +22,12 @@ "path": { "all": true }, + "notification": { + "all": true + }, + "os": { + "all": true + }, "http": { "scope": [ "https://ifconfig.me/*", diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 2b10f65..278a1b0 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,5 +1,6 @@ -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { QueryClientProvider } from '@tanstack/react-query' +import { sendNotification } from '@tauri-apps/api/notification' import { Offline, Online } from 'react-detect-offline' import { toast } from 'react-hot-toast' @@ -12,6 +13,7 @@ import { OfflineScreen } from './screens/OfflineScreen' import { AuthScreen } from './screens/AuthScreen' import { MainScreen } from './screens/MainScreen' import { queryClient } from './utils/queries' +import { useNotification } from './hooks/useNotification' const offlineDetectConfig = { url: 'https://ifconfig.me/ip', @@ -22,6 +24,7 @@ const offlineDetectConfig = { export default function App() { const session = useAuth() + const allowNotification = useNotification() useEffect(() => { disableBrowserEvents('contextmenu') @@ -29,11 +32,21 @@ export default function App() { }) const handleOffline = (online: boolean) => { - !online && toast.error('Your internet connection is lost!') + if (!online) { + const message = 'Your internet connection is lost!' + return allowNotification + ? sendNotification({ title: 'Otentik Authenticator', body: message }) + : toast.error(message) + } } const handleOnline = (online: boolean) => { - online && toast.success('Your internet connection is back!') + if (online) { + const message = 'Your internet connection is back!' + return allowNotification + ? sendNotification({ title: 'Otentik Authenticator', body: message }) + : toast.success(message) + } } if (!session) return @@ -42,12 +55,22 @@ export default function App() {
- handleOffline(online)}> + { + handleOffline(online) + }} + > - handleOnline(online)}> + { + handleOnline(online) + }} + > diff --git a/apps/desktop/src/components/AppMenu.tsx b/apps/desktop/src/components/AppMenu.tsx index 4dced4f..5c98d55 100644 --- a/apps/desktop/src/components/AppMenu.tsx +++ b/apps/desktop/src/components/AppMenu.tsx @@ -1,5 +1,6 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { Fragment, useEffect, useState } from 'react' -import { appWindow } from '@tauri-apps/api/window' +import { invoke } from '@tauri-apps/api/tauri' import { ask, open, save } from '@tauri-apps/api/dialog' import { listen } from '@tauri-apps/api/event' import type { EventName } from '@tauri-apps/api/event' @@ -34,60 +35,6 @@ export const AppMenu = () => { const [menuPayload, setMenuPayload] = useState('') const [listenTauriEvent, setListenTauriEvent] = useState(false) - useEffect(() => { - // Listen tauri event - listen('menu-event', (e) => { - console.log('LISTEN', e.payload) - setMenuPayload(e.payload) - setListenTauriEvent(true) - }) - }, [setMenuPayload, setListenTauriEvent]) - - useEffect(() => { - if (listenTauriEvent) { - switch (menuPayload) { - case 'quit': - handleQuit() - break - - case 'close': - handleQuit() - break - - case 'export': - handleExport() - break - - case 'import': - handleImport() - break - - case 'lock_vault': - setLockStreenState(true) - break - - case 'new_item': - setFormCreateOpen(true) - break - - case 'signout': - handleSignOut() - break - - case 'sync_vault': - setForceFetch(true) - break - - case 'update_check': - break - - default: - break - } - } - setListenTauriEvent(false) - }, [listenTauriEvent, menuPayload]) - // Reset all states before quit. const resetStates = () => { setLockStreenState(true) @@ -136,7 +83,7 @@ export const AppMenu = () => { resetStates() // wait for screen locked before quitting await delay(1000) - appWindow.close() + await invoke('exit_app') } } @@ -148,6 +95,56 @@ export const AppMenu = () => { } } + useEffect(() => { + // Listen tauri event + listen('app-event', (e) => { + setMenuPayload(e.payload) + setListenTauriEvent(true) + }) + }, [setMenuPayload, setListenTauriEvent]) + + useEffect(() => { + if (listenTauriEvent) { + switch (menuPayload) { + case 'quit': + handleQuit() + break + + case 'close': + handleQuit() + break + + case 'export': + handleExport() + break + + case 'import': + handleImport() + break + + case 'lock_vault': + setLockStreenState(true) + break + + case 'new_item': + setFormCreateOpen(true) + break + + case 'signout': + handleSignOut() + break + + case 'sync_vault': + setForceFetch(true) + break + + default: + break + } + } + setListenTauriEvent(false) + }, [listenTauriEvent, menuPayload]) + return (
diff --git a/apps/desktop/src/components/EmptyState.tsx b/apps/desktop/src/components/EmptyState.tsx index 06aeb14..ab94b50 100644 --- a/apps/desktop/src/components/EmptyState.tsx +++ b/apps/desktop/src/components/EmptyState.tsx @@ -3,6 +3,7 @@ import { useStores } from '../stores/stores' export const EmptyState = () => { const setFormCreateOpen = useStores((state) => state.setFormCreateOpen) + const handleAction = async () => setFormCreateOpen(true) return (
@@ -13,7 +14,7 @@ export const EmptyState = () => {

+

+ Forgot password?{' '} + + Reset + +

diff --git a/apps/desktop/src/screens/LockScreen.tsx b/apps/desktop/src/screens/LockScreen.tsx index 109e6b5..fcf27ab 100644 --- a/apps/desktop/src/screens/LockScreen.tsx +++ b/apps/desktop/src/screens/LockScreen.tsx @@ -3,20 +3,18 @@ import { Dialog } from '@headlessui/react' import { EyeIcon, EyeSlashIcon, LockClosedIcon } from '@heroicons/react/24/outline' import { ExclamationCircleIcon } from '@heroicons/react/24/solid' -import { useAuth } from '../hooks/useAuth' import { useStores } from '../stores/stores' import { classNames } from '../utils/ui-helpers' import { DialogTransition } from '../components/DialogTransition' -import { md5Hash, verifyHash } from '../utils/string-helpers' import { LoaderScreen } from '../components/LoaderScreen' -import { localData } from '../utils/storage' +import { updateDeviceInfo } from '../utils/supabase' +import { validatePassphrase } from '../utils/guards' export const LockScreen = () => { - const session = useAuth() const locked = useStores((state) => state.locked) const setLockStreenState = useStores((state) => state.setLockStreenState) const [error, setError] = useState({ error: null, text: null }) - const [passphrase, setPassphrase] = useState('') + const [password, setPassphrase] = useState('') const [inputType, setInputType] = useState('password') const [loading, setLoading] = useState(false) @@ -27,22 +25,21 @@ export const LockScreen = () => { const handleUnlockAction = async (e: React.FormEvent) => { e.preventDefault() - if (passphrase.length <= 1) { + if (password.length <= 1) { return setError({ error: true, text: 'Your password required!' }) } setLoading(true) - const passphraseHash = session?.user?.user_metadata?.passphrase - const validPassphrase = await verifyHash(passphrase, passphraseHash) + // Compare with hashed passphrase in localStorage + const validPassphrase = await validatePassphrase(password) + setLoading(false) if (!validPassphrase) { - setLoading(false) return setError({ error: true, text: 'Invalid password!' }) } - // Store hashed passphrase in localStorage - const hashedPassphrase = await md5Hash(passphrase) - await localData.set('passphrase', hashedPassphrase) + // Update device information. + await updateDeviceInfo() setError(null) setLoading(false) @@ -68,14 +65,14 @@ export const LockScreen = () => {
-