Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Auth Tokens storage from Local Storage to custom encrypted .sdks file #2831

Merged
merged 4 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

70 changes: 46 additions & 24 deletions apps/desktop/crates/macos/src-swift/window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ import SwiftRs

@objc
public enum AppThemeType: Int {
case auto = -1
case light = 0
case dark = 1
case auto = -1
case light = 0
case dark = 1
}

var activity: NSObjectProtocol?
private let activityLock = NSLock()
private var activity: NSObjectProtocol?
private var isThemeUpdating = false

@_cdecl("disable_app_nap")
public func disableAppNap(reason: SRString) -> Bool {
// Check if App Nap is already disabled
activityLock.lock()
defer { activityLock.unlock() }

guard activity == nil else {
return false
}
Expand All @@ -26,37 +30,55 @@ public func disableAppNap(reason: SRString) -> Bool {

@_cdecl("enable_app_nap")
public func enableAppNap() -> Bool {
// Check if App Nap is already enabled
guard let pinfo = activity else {
activityLock.lock()
defer { activityLock.unlock() }

guard let currentActivity = activity else {
return false
}

ProcessInfo.processInfo.endActivity(pinfo)
ProcessInfo.processInfo.endActivity(currentActivity)
activity = nil
return true
}

@_cdecl("lock_app_theme")
public func lockAppTheme(themeType: AppThemeType) {
var theme: NSAppearance?
switch themeType {
case .auto:
theme = nil
case .dark:
theme = NSAppearance(named: .darkAqua)!
case .light:
theme = NSAppearance(named: .aqua)!
}
// Prevent concurrent theme updates
guard !isThemeUpdating else {
return
}

DispatchQueue.main.async {
NSApp.appearance = theme
isThemeUpdating = true

// Trigger a repaint of the window
if let window = NSApplication.shared.mainWindow {
window.invalidateShadow()
window.displayIfNeeded()
let theme: NSAppearance?
switch themeType {
case .auto:
theme = nil
case .dark:
theme = NSAppearance(named: .darkAqua)
case .light:
theme = NSAppearance(named: .aqua)
}

// Use sync to ensure completion before return
DispatchQueue.main.sync {
autoreleasepool {
NSApp.appearance = theme

if let window = NSApplication.shared.mainWindow {
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0
window.invalidateShadow()
window.displayIfNeeded()
}, completionHandler: {
isThemeUpdating = false
})
} else {
isThemeUpdating = false
}
}
}
}
}

@_cdecl("set_titlebar_style")
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"react-dom": "^18.2.0",
"react-router-dom": "=6.20.1",
"sonner": "^1.0.3",
"supertokens-web-js": "^0.13.0"
"supertokens-web-js": "=0.13.0"
},
"devDependencies": {
"@sd/config": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ uuid = { workspace = true, features = ["serde"] }
opener = { version = "0.7.1", features = ["reveal"], default-features = false }
specta-typescript = "=0.0.7"
tauri-plugin-clipboard-manager = "=2.0.1"
tauri-plugin-cors-fetch = { path = "../../../crates/tauri-plugin-cors-fetch" }
tauri-plugin-deep-link = "=2.0.1"
tauri-plugin-dialog = "=2.0.3"
tauri-plugin-http = "=2.0.3"
Expand All @@ -47,7 +48,6 @@ tauri-plugin-updater = "=2.0.2"

# memory allocator
mimalloc = { workspace = true }
tauri-plugin-cors-fetch = "2.1.1"

[dependencies.tauri]
features = ["linux-libxdo", "macos-private-api", "native-tls-vendored", "unstable"]
Expand Down
10 changes: 6 additions & 4 deletions apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,15 @@ type RedirectPath = { pathname: string; search: string | undefined };
function AppInner() {
const [tabs, setTabs] = useState(() => [createTab()]);
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const tokens = getTokens();
const cloudBootstrap = useBridgeMutation('cloud.bootstrap');

useEffect(() => {
// If the access token and/or refresh token are missing, we need to skip the cloud bootstrap
if (tokens.accessToken.length === 0 || tokens.refreshToken.length === 0) return;
cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]);
(async () => {
const tokens = await getTokens();
// If the access token and/or refresh token are missing, we need to skip the cloud bootstrap
if (tokens.accessToken.length === 0 || tokens.refreshToken.length === 0) return;
cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]);
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Expand Down
186 changes: 186 additions & 0 deletions core/src/api/keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use super::{Ctx, SanitizedNodeConfig, R};
use rspc::{alpha::AlphaRouter, ErrorCode};
use sd_crypto::cookie::CookieCipher;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::io::AsyncWriteExt;
use tokio::sync::RwLock;
use tracing::{debug, error};

#[derive(Clone)]
struct CipherCache {
uuid: String,
cipher: CookieCipher,
}

async fn get_cipher(
node: &Ctx,
cache: Arc<RwLock<Option<CipherCache>>>,
) -> Result<CookieCipher, rspc::Error> {
let config = SanitizedNodeConfig::from(node.config.get().await);
let uuid = config.id.to_string();

{
let cache_read = cache.read().await;
if let Some(ref cache) = *cache_read {
if cache.uuid == uuid {
return Ok(cache.cipher.clone());
}
}
}

let uuid_key = CookieCipher::generate_key_from_string(&uuid).map_err(|e| {
error!("Failed to generate key: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to generate key".to_string(),
)
})?;

let cipher = CookieCipher::new(&uuid_key).map_err(|e| {
error!("Failed to create cipher: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to create cipher".to_string(),
)
})?;

{
let mut cache_write = cache.write().await;
*cache_write = Some(CipherCache {
uuid,
cipher: cipher.clone(),
});
}

Ok(cipher)
}

async fn read_file(path: &Path) -> Result<Vec<u8>, rspc::Error> {
tokio::fs::read(path).await.map_err(|e| {
error!("Failed to read file: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
format!("Failed to read file {:?}", path),
)
})
}

async fn write_file(path: &Path, data: &[u8]) -> Result<(), rspc::Error> {
let mut file = tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.await
.map_err(|e| {
error!("Failed to open file: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
format!("Failed to open file {:?}", path),
)
})?;
file.write_all(data).await.map_err(|e| {
error!("Failed to write to file: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
format!("Failed to write to file {:?}", path),
)
})
}

fn sanitize_path(base_dir: &Path, path: &Path) -> Result<PathBuf, rspc::Error> {
let abs_base = base_dir.canonicalize().map_err(|e| {
error!("Failed to canonicalize base directory: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to canonicalize base directory".to_string(),
)
})?;
let abs_path = abs_base.join(path).canonicalize().map_err(|e| {
error!("Failed to canonicalize path: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to canonicalize path".to_string(),
)
})?;
if abs_path.starts_with(&abs_base) {
Ok(abs_path)
} else {
error!("Path injection attempt detected: {:?}", abs_path);
Err(rspc::Error::new(
ErrorCode::InternalServerError,
"Invalid path".to_string(),
))
}
}

pub(crate) fn mount() -> AlphaRouter<Ctx> {
let cipher_cache = Arc::new(RwLock::new(None));

R.router()
.procedure("get", {
let cipher_cache = cipher_cache.clone();
R.query(move |node, _: ()| {
let cipher_cache = cipher_cache.clone();
async move {
let base_dir = node.config.data_directory();
let path = sanitize_path(&base_dir, Path::new(".sdks"))?;
let data = read_file(&path).await?;
let cipher = get_cipher(&node, cipher_cache).await?;

let data_str = String::from_utf8(data).map_err(|e| {
error!("Failed to convert data to string: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to convert data to string".to_string(),
)
})?;
let data = CookieCipher::base64_decode(&data_str).map_err(|e| {
error!("Failed to decode data: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to decode data".to_string(),
)
})?;
let de_data = cipher.decrypt(&data).map_err(|e| {
error!("Failed to decrypt data: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to decrypt data".to_string(),
)
})?;
let de_data = String::from_utf8(de_data).map_err(|e| {
error!("Failed to convert data to string: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to convert data to string".to_string(),
)
})?;
Ok(de_data)
}
})
})
.procedure("save", {
let cipher_cache = cipher_cache.clone();
R.mutation(move |node, args: String| {
let cipher_cache = cipher_cache.clone();
async move {
let cipher = get_cipher(&node, cipher_cache).await?;
let en_data = cipher.encrypt(args.as_bytes()).map_err(|e| {
error!("Failed to encrypt data: {:?}", e.to_string());
rspc::Error::new(
ErrorCode::InternalServerError,
"Failed to encrypt data".to_string(),
)
})?;
let en_data = CookieCipher::base64_encode(&en_data);
let base_dir = node.config.data_directory();
let path = sanitize_path(&base_dir, Path::new(".sdks"))?;
write_file(&path, en_data.as_bytes()).await?;
debug!("Saved data to {:?}", path);
Ok(())
}
})
})
}
2 changes: 2 additions & 0 deletions core/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ mod cloud;
mod ephemeral_files;
mod files;
mod jobs;
mod keys;
mod labels;
mod libraries;
pub mod locations;
Expand Down Expand Up @@ -210,6 +211,7 @@ pub(crate) fn mount() -> Arc<Router> {
.merge("preferences.", preferences::mount())
.merge("notifications.", notifications::mount())
.merge("backups.", backups::mount())
.merge("keys.", keys::mount())
.merge("invalidation.", utils::mount_invalidate())
.sd_patch_types_dangerously(|type_map| {
let def =
Expand Down
Loading
Loading