feat(desktop): isolate private data by account
This commit is contained in:
parent
3e317745b5
commit
1d2a8a2f63
12 changed files with 143 additions and 60 deletions
|
|
@ -0,0 +1,88 @@
|
|||
//! 登录后的账号隔离存储根。
|
||||
//!
|
||||
//! 私人频道、知识库和代码频道不得在编号与仓库账号验证完成前读取,
|
||||
//! 也不得让两个仓库账号共享同一份本机数据目录。
|
||||
|
||||
use ring::digest::{digest, SHA256};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::code_repo_login::{self, LoginSession};
|
||||
|
||||
pub(crate) fn account_storage_root(app: &AppHandle, namespace: &str) -> Result<PathBuf, String> {
|
||||
let session = code_repo_login::current_login_session(app)?
|
||||
.ok_or_else(|| "HOLOLAKE_AUTHENTICATED_ACCOUNT_REQUIRED".to_string())?;
|
||||
let account_id = account_scope_id(&session)?;
|
||||
let root = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
|
||||
.join("accounts-v1")
|
||||
.join(account_id)
|
||||
.join(validate_namespace(namespace)?);
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_ACCOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&root, fs::Permissions::from_mode(0o700))
|
||||
.map_err(|error| format!("HOLOLAKE_ACCOUNT_STORAGE_PERMISSION_FAILED: {error}"))?;
|
||||
}
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
fn validate_namespace(namespace: &str) -> Result<&str, String> {
|
||||
if !namespace.is_empty()
|
||||
&& namespace.len() <= 48
|
||||
&& namespace
|
||||
.chars()
|
||||
.all(|item| item.is_ascii_lowercase() || item.is_ascii_digit() || item == '-')
|
||||
{
|
||||
Ok(namespace)
|
||||
} else {
|
||||
Err("HOLOLAKE_ACCOUNT_STORAGE_NAMESPACE_INVALID".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn account_scope_id(session: &LoginSession) -> Result<String, String> {
|
||||
if session.username.is_empty() || session.host.is_empty() || session.domain.is_empty() {
|
||||
return Err("HOLOLAKE_LOGIN_SESSION_SCOPE_INVALID".into());
|
||||
}
|
||||
let material = format!("{}\0{}\0{}", session.domain, session.host, session.username);
|
||||
let value = digest(&SHA256, material.as_bytes());
|
||||
Ok(format!("account-{}", hex(value.as_ref())))
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> String {
|
||||
bytes.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn session(username: &str) -> LoginSession {
|
||||
LoginSession {
|
||||
username: username.into(),
|
||||
host: "guanghulab.com".into(),
|
||||
domain: "FIFTH_DOMAIN".into(),
|
||||
signed_in_at_unix_ms: 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_accounts_never_share_a_storage_scope() {
|
||||
assert_ne!(
|
||||
account_scope_id(&session("bingshuo")).unwrap(),
|
||||
account_scope_id(&session("another-human")).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_scope_is_stable_and_does_not_expose_the_username() {
|
||||
let first = account_scope_id(&session("bingshuo")).unwrap();
|
||||
assert_eq!(first, account_scope_id(&session("bingshuo")).unwrap());
|
||||
assert!(!first.contains("bingshuo"));
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
|||
use std::path::{Component, Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -175,11 +175,7 @@ pub async fn read_code_channel_file(
|
|||
}
|
||||
|
||||
fn code_channel_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
|
||||
.join("code-channel-v1");
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "code-channel-v1")?;
|
||||
ensure_root(&root)?;
|
||||
Ok(root)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::fs::{self, OpenOptions};
|
|||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const SESSION_SCHEMA: &str = "hololake.direct-local-session/v1";
|
||||
|
|
@ -192,11 +192,7 @@ pub async fn append_direct_local_session_event(
|
|||
}
|
||||
|
||||
pub(crate) fn direct_session_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let app_data = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?;
|
||||
let root = app_data.join("direct-local-session-v1");
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "direct-local-session-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("HOLOLAKE_DIRECT_SESSION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
root.canonicalize()
|
||||
|
|
|
|||
|
|
@ -452,22 +452,10 @@ pub async fn select_and_import_knowledge_folder(
|
|||
}
|
||||
|
||||
fn knowledge_roots(app: &AppHandle) -> Result<(PathBuf, Option<PathBuf>), String> {
|
||||
let native = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?
|
||||
.join("knowledge-v1");
|
||||
let native = crate::authenticated_storage::account_storage_root(app, "knowledge-v1")?;
|
||||
ensure_native_knowledge_root(&native)?;
|
||||
let legacy = dirs::home_dir()
|
||||
.map(|home| {
|
||||
home.join("Library")
|
||||
.join("Application Support")
|
||||
.join("hololake-desktop")
|
||||
.join("data")
|
||||
.join("knowledge-base")
|
||||
})
|
||||
.filter(|path| path.join("docs").is_dir());
|
||||
Ok((native, legacy))
|
||||
// 旧全局知识库不得自动投影给任一新登录账号;后续只允许用户显式迁移。
|
||||
Ok((native, None))
|
||||
}
|
||||
|
||||
/// 写私有文件的跨平台选项:unix 上只许本人读写,Windows 上不做额外权限设置。
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
mod circular_lake_membrane;
|
||||
mod authenticated_storage;
|
||||
mod code_channel;
|
||||
mod code_repo_login;
|
||||
mod direct_local_broker;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1";
|
||||
|
|
@ -148,11 +148,7 @@ pub async fn transition_personal_channel_task(
|
|||
}
|
||||
|
||||
fn personal_channel_database(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let app_data = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("HOLOLAKE_APP_DATA_UNAVAILABLE: {error}"))?;
|
||||
let root = app_data.join("personal-channel-v1");
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "personal-channel-v1")?;
|
||||
create_private_directory(&root)?;
|
||||
Ok(root.join("personal-channel.sqlite3"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use std::fs::{self, OpenOptions};
|
|||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
const JOURNAL_SCHEMA: &str = "hololake.pncc-stage-one-receipt-projection-journal/v1";
|
||||
|
|
@ -90,12 +90,7 @@ pub async fn query_pncc_receipt_projection(
|
|||
}
|
||||
|
||||
pub(crate) fn pncc_projection_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("PNCC_APP_DATA_UNAVAILABLE: {error}"))?
|
||||
.join("pncc-stage-one-v1")
|
||||
.join("receipt-projection");
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "pncc-receipt-projection-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("PNCC_PROJECTION_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
root.canonicalize()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use std::os::unix::fs::OpenOptionsExt;
|
|||
use std::path::{Component, Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -153,12 +153,7 @@ pub async fn confirm_pncc_repository_mount(
|
|||
}
|
||||
|
||||
pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("PNCC_APP_DATA_UNAVAILABLE: {error}"))?
|
||||
.join("pncc-stage-one-v1")
|
||||
.join("repository-mounts");
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "pncc-repository-mounts-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("PNCC_REPOSITORY_MOUNT_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
root.canonicalize()
|
||||
|
|
@ -166,12 +161,7 @@ pub(crate) fn pncc_repository_mount_root(app: &AppHandle) -> Result<PathBuf, Str
|
|||
}
|
||||
|
||||
fn pncc_repository_candidate_root(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let root = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("PNCC_APP_DATA_UNAVAILABLE: {error}"))?
|
||||
.join("pncc-stage-one-v1")
|
||||
.join("repository-candidates");
|
||||
let root = crate::authenticated_storage::account_storage_root(app, "pncc-repository-candidates-v1")?;
|
||||
fs::create_dir_all(&root)
|
||||
.map_err(|error| format!("PNCC_REPOSITORY_CANDIDATE_STORAGE_UNAVAILABLE: {error}"))?;
|
||||
root.canonicalize()
|
||||
|
|
|
|||
Loading…
Reference in a new issue