2026-03-16 12:58:35 +00:00
|
|
|
use hmac::{Hmac, Mac};
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
use rand::RngCore;
|
2026-03-06 03:26:56 +00:00
|
|
|
use sha2::{Digest, Sha256};
|
|
|
|
|
use std::collections::HashMap;
|
2026-03-16 12:58:35 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
2026-03-06 03:26:56 +00:00
|
|
|
use std::sync::Arc;
|
2026-03-22 03:30:21 +00:00
|
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
2026-03-31 11:06:19 +01:00
|
|
|
use tokio::sync::{OnceCell, RwLock};
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
use zeroize::Zeroize;
|
|
|
|
|
|
2026-03-31 11:06:19 +01:00
|
|
|
/// Cached remember secret — loaded once, never regenerated within a process.
|
|
|
|
|
static REMEMBER_SECRET: OnceCell<Vec<u8>> = OnceCell::const_new();
|
|
|
|
|
|
2026-03-16 12:58:35 +00:00
|
|
|
type HmacSha256 = Hmac<Sha256>;
|
|
|
|
|
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
const FULL_SESSION_TTL: u64 = 86400; // 24 hours of inactivity
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
const PENDING_SESSION_TTL: u64 = 300; // 5 minutes
|
|
|
|
|
const MAX_TOTP_ATTEMPTS: u8 = 5;
|
feat: rootless podman, session hardening, boot stability, sidebar fix
Rootless podman migration (TASK-11):
- Remove sudo from all podman calls in PodmanClient + 8 backend files
- Remove sudo from all podman/docker calls in deploy script
- Restore full systemd security hardening: NoNewPrivileges,
RestrictAddressFamilies, MemoryDenyWriteExecute, RestrictRealtime,
RestrictNamespaces, RestrictSUIDSGID, SystemCallFilter, ProtectSystem=strict
- Enable loginctl linger for rootless container persistence
- Remove Ollama from auto-deploy (marketplace-only)
Session & auth hardening:
- Increase MAX_CONCURRENT_SESSIONS 20→50 (prevents eviction storms)
- Debounced 401 redirect in rpc-client.ts (prevents redirect storms)
Boot stability:
- optimize-debian.sh: adds chrony, swap, removes policy-rc.d
- deploy script: pre-restart chrony + swap setup
- ISO build: chrony package, swap file creation
- BootScreen: no longer clears localStorage (prevents splash replay)
- RootRedirect: sole owner of localStorage clearing on server ready
UI fixes:
- Sidebar opacity default changed from 0→visible (fixes missing sidebar
after page-persistence login without entrance animation)
- Console.log/error wrapped in import.meta.env.DEV guards
- Remove unused route import from RootRedirect
Beta tracking:
- CLAUDE.md: beta freeze protocol added
- MASTER_PLAN.md: TASK-11, TASK-17, phase structure
- BETA-PROGRESS.md: initial tracking doc
- Tagged v1.2.0-alpha.1 as pre-rootless baseline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 13:53:27 +00:00
|
|
|
const MAX_CONCURRENT_SESSIONS: usize = 50;
|
2026-03-16 12:58:35 +00:00
|
|
|
const SESSIONS_FILE: &str = "/var/lib/archipelago/sessions.json";
|
|
|
|
|
const REMEMBER_SECRET_FILE: &str = "/var/lib/archipelago/remember_secret";
|
|
|
|
|
pub const REMEMBER_TTL: u64 = 30 * 24 * 3600; // 30 days
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
enum SessionType {
|
|
|
|
|
Full,
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
PendingTotp { totp_secret: Vec<u8>, attempts: u8 },
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Drop for SessionType {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
if let SessionType::PendingTotp { totp_secret, .. } = self {
|
|
|
|
|
totp_secret.zeroize();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-06 03:26:56 +00:00
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
struct Session {
|
2026-03-15 04:32:24 +00:00
|
|
|
created_at: SystemTime,
|
|
|
|
|
last_activity: SystemTime,
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
session_type: SessionType,
|
2026-03-06 03:26:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct SessionStore {
|
|
|
|
|
sessions: Arc<RwLock<HashMap<[u8; 32], Session>>>,
|
2026-03-16 12:58:35 +00:00
|
|
|
persist_path: PathBuf,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// On-disk representation of a persisted session (only Full sessions, no TOTP secrets).
|
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize)]
|
|
|
|
|
struct PersistedSession {
|
|
|
|
|
hash_hex: String,
|
|
|
|
|
created_at: u64, // Unix timestamp
|
|
|
|
|
last_activity: u64, // Unix timestamp
|
2026-03-06 03:26:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SessionStore {
|
2026-03-21 01:21:08 +00:00
|
|
|
pub async fn new() -> Self {
|
2026-03-16 12:58:35 +00:00
|
|
|
let persist_path = PathBuf::from(SESSIONS_FILE);
|
2026-03-21 01:21:08 +00:00
|
|
|
let sessions = Self::load_from_disk(&persist_path).await;
|
2026-03-16 12:58:35 +00:00
|
|
|
let count = sessions.len();
|
|
|
|
|
if count > 0 {
|
|
|
|
|
tracing::info!("Restored {} sessions from disk", count);
|
|
|
|
|
}
|
2026-03-06 03:26:56 +00:00
|
|
|
Self {
|
2026-03-16 12:58:35 +00:00
|
|
|
sessions: Arc::new(RwLock::new(sessions)),
|
|
|
|
|
persist_path,
|
2026-03-06 03:26:56 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-16 12:58:35 +00:00
|
|
|
/// Load persisted sessions from disk (only Full sessions).
|
2026-03-21 01:21:08 +00:00
|
|
|
async fn load_from_disk(path: &Path) -> HashMap<[u8; 32], Session> {
|
2026-03-16 12:58:35 +00:00
|
|
|
let mut map = HashMap::new();
|
2026-03-21 01:21:08 +00:00
|
|
|
let data = match tokio::fs::read_to_string(path).await {
|
2026-03-16 12:58:35 +00:00
|
|
|
Ok(d) => d,
|
|
|
|
|
Err(_) => return map,
|
|
|
|
|
};
|
|
|
|
|
let persisted: Vec<PersistedSession> = match serde_json::from_str(&data) {
|
|
|
|
|
Ok(v) => v,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
tracing::warn!("Failed to parse sessions file: {}", e);
|
|
|
|
|
return map;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let now_unix = SystemTime::now()
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_secs();
|
|
|
|
|
for p in persisted {
|
|
|
|
|
// Skip expired sessions
|
|
|
|
|
if now_unix.saturating_sub(p.last_activity) >= FULL_SESSION_TTL {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let hash = match hex::decode(&p.hash_hex) {
|
|
|
|
|
Ok(h) if h.len() == 32 => {
|
|
|
|
|
let mut arr = [0u8; 32];
|
|
|
|
|
arr.copy_from_slice(&h);
|
|
|
|
|
arr
|
|
|
|
|
}
|
|
|
|
|
_ => continue,
|
|
|
|
|
};
|
|
|
|
|
let created_at = UNIX_EPOCH + std::time::Duration::from_secs(p.created_at);
|
|
|
|
|
let last_activity = UNIX_EPOCH + std::time::Duration::from_secs(p.last_activity);
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
map.insert(
|
|
|
|
|
hash,
|
|
|
|
|
Session {
|
|
|
|
|
created_at,
|
|
|
|
|
last_activity,
|
|
|
|
|
session_type: SessionType::Full,
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-03-16 12:58:35 +00:00
|
|
|
}
|
|
|
|
|
map
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Save all Full sessions to disk. Called after mutations.
|
2026-03-21 01:21:08 +00:00
|
|
|
async fn save_to_disk(sessions: &HashMap<[u8; 32], Session>, path: &Path) {
|
2026-03-16 12:58:35 +00:00
|
|
|
let persisted: Vec<PersistedSession> = sessions
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|(_, s)| matches!(s.session_type, SessionType::Full))
|
|
|
|
|
.map(|(hash, s)| PersistedSession {
|
|
|
|
|
hash_hex: hex::encode(hash),
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
created_at: s
|
|
|
|
|
.created_at
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_secs(),
|
|
|
|
|
last_activity: s
|
|
|
|
|
.last_activity
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_secs(),
|
2026-03-16 12:58:35 +00:00
|
|
|
})
|
|
|
|
|
.collect();
|
|
|
|
|
if let Ok(json) = serde_json::to_string(&persisted) {
|
2026-03-21 01:21:08 +00:00
|
|
|
let _ = tokio::fs::write(path, json).await;
|
2026-03-16 12:58:35 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
/// Create a full (authenticated) session. Returns the plaintext token.
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
/// Enforces max concurrent sessions by evicting the oldest if limit reached.
|
2026-03-06 03:26:56 +00:00
|
|
|
pub async fn create(&self) -> String {
|
|
|
|
|
let token_bytes: [u8; 32] = rand::random();
|
|
|
|
|
let token = hex::encode(token_bytes);
|
|
|
|
|
let hash = hash_token(&token);
|
2026-03-15 04:32:24 +00:00
|
|
|
let now = SystemTime::now();
|
2026-03-06 03:26:56 +00:00
|
|
|
let session = Session {
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
created_at: now,
|
|
|
|
|
last_activity: now,
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
session_type: SessionType::Full,
|
|
|
|
|
};
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
|
|
|
|
|
let mut sessions = self.sessions.write().await;
|
|
|
|
|
self.evict_if_over_limit(&mut sessions);
|
|
|
|
|
sessions.insert(hash, session);
|
2026-03-16 12:58:35 +00:00
|
|
|
// Sync save — must complete before returning the token to the client.
|
|
|
|
|
// Async save risks losing the session if the process is killed (e.g., deploy restart).
|
2026-03-21 01:21:08 +00:00
|
|
|
Self::save_to_disk(&sessions, &self.persist_path).await;
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
token
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Create a pending TOTP session (password verified, awaiting TOTP).
|
|
|
|
|
/// Caches the decrypted TOTP secret in memory for verification.
|
|
|
|
|
pub async fn create_pending(&self, totp_secret: Vec<u8>) -> String {
|
|
|
|
|
let token_bytes: [u8; 32] = rand::random();
|
|
|
|
|
let token = hex::encode(token_bytes);
|
|
|
|
|
let hash = hash_token(&token);
|
2026-03-15 04:32:24 +00:00
|
|
|
let now = SystemTime::now();
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
let session = Session {
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
created_at: now,
|
|
|
|
|
last_activity: now,
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
session_type: SessionType::PendingTotp {
|
|
|
|
|
totp_secret,
|
|
|
|
|
attempts: 0,
|
|
|
|
|
},
|
2026-03-06 03:26:56 +00:00
|
|
|
};
|
|
|
|
|
self.sessions.write().await.insert(hash, session);
|
|
|
|
|
token
|
|
|
|
|
}
|
|
|
|
|
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
/// Validate a full session token. Returns true if the session exists and hasn't expired.
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
/// Updates last_activity on successful validation (inactivity-based expiry).
|
2026-03-06 03:26:56 +00:00
|
|
|
pub async fn validate(&self, token: &str) -> bool {
|
|
|
|
|
let hash = hash_token(token);
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
let mut sessions = self.sessions.write().await;
|
|
|
|
|
if let Some(session) = sessions.get_mut(&hash) {
|
|
|
|
|
if !matches!(session.session_type, SessionType::Full) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
if session
|
|
|
|
|
.last_activity
|
|
|
|
|
.elapsed()
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_secs()
|
|
|
|
|
>= FULL_SESSION_TTL
|
|
|
|
|
{
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
sessions.remove(&hash);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-03-15 04:32:24 +00:00
|
|
|
session.last_activity = SystemTime::now();
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
true
|
2026-03-06 03:26:56 +00:00
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
/// Get the TOTP secret from a pending session. Returns None if not a valid pending session.
|
|
|
|
|
/// Increments the attempt counter.
|
|
|
|
|
pub async fn get_pending_secret(&self, token: &str) -> Option<Vec<u8>> {
|
|
|
|
|
let hash = hash_token(token);
|
|
|
|
|
let mut sessions = self.sessions.write().await;
|
|
|
|
|
if let Some(session) = sessions.get_mut(&hash) {
|
2026-03-15 04:32:24 +00:00
|
|
|
if session.created_at.elapsed().unwrap_or_default().as_secs() >= PENDING_SESSION_TTL {
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
sessions.remove(&hash);
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
if let SessionType::PendingTotp {
|
|
|
|
|
ref totp_secret,
|
|
|
|
|
ref mut attempts,
|
|
|
|
|
} = session.session_type
|
|
|
|
|
{
|
|
|
|
|
*attempts += 1;
|
|
|
|
|
if *attempts > MAX_TOTP_ATTEMPTS {
|
|
|
|
|
sessions.remove(&hash); // Too many attempts, force re-login
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
return Some(totp_secret.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
/// Upgrade a pending session to a full session with token rotation.
|
|
|
|
|
/// Deletes the old pending session and creates a new full session with a fresh token.
|
|
|
|
|
/// Returns the new plaintext token so the caller can set it as the new cookie.
|
|
|
|
|
pub async fn upgrade_to_full(&self, token: &str) -> Option<String> {
|
|
|
|
|
let old_hash = hash_token(token);
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
let mut sessions = self.sessions.write().await;
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
// Only upgrade if the old session exists and is pending
|
|
|
|
|
if sessions.remove(&old_hash).is_some() {
|
|
|
|
|
let new_token_bytes: [u8; 32] = rand::random();
|
|
|
|
|
let new_token = hex::encode(new_token_bytes);
|
|
|
|
|
let new_hash = hash_token(&new_token);
|
2026-03-15 04:32:24 +00:00
|
|
|
let now = SystemTime::now();
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
self.evict_if_over_limit(&mut sessions);
|
|
|
|
|
sessions.insert(
|
|
|
|
|
new_hash,
|
|
|
|
|
Session {
|
|
|
|
|
created_at: now,
|
|
|
|
|
last_activity: now,
|
|
|
|
|
session_type: SessionType::Full,
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-03-21 01:21:08 +00:00
|
|
|
Self::save_to_disk(&sessions, &self.persist_path).await;
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
Some(new_token)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
feat: add TOTP 2FA, API key switcher, login progress bar, and alpha hardening plan
- TOTP 2FA: full setup/confirm/disable/login flow with Argon2id + ChaCha20-Poly1305
encrypted secret storage, QR code generation, and bcrypt-hashed backup codes
- API key switcher: OAuth vs personal API key toggle in AIUI chat settings with
status indicator, key validation, and help text
- Login progress bar: server startup detection with health check polling, form
disabled until server is ready
- AI quarantine docs: comprehensive HTML page documenting all 6 security layers
- Settings: AI Data Access permission toggles with per-category control
- Alpha hardening plan: 28-task overnight automation plan across 7 phases
(onboarding, login, app install, AIUI, UI polish, security, ISO build)
- Backlog: node discovery spatial map feature for alpha demo
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 12:23:57 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 03:26:56 +00:00
|
|
|
pub async fn remove(&self, token: &str) {
|
|
|
|
|
let hash = hash_token(token);
|
2026-03-16 12:58:35 +00:00
|
|
|
let mut sessions = self.sessions.write().await;
|
|
|
|
|
sessions.remove(&hash);
|
2026-03-21 01:21:08 +00:00
|
|
|
Self::save_to_disk(&sessions, &self.persist_path).await;
|
2026-03-06 03:26:56 +00:00
|
|
|
}
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
|
|
|
|
|
/// Invalidate all sessions except the one matching the given token.
|
|
|
|
|
/// Used after sensitive operations like password change.
|
|
|
|
|
pub async fn invalidate_all_except(&self, keep_token: &str) {
|
|
|
|
|
let keep_hash = hash_token(keep_token);
|
|
|
|
|
let mut sessions = self.sessions.write().await;
|
|
|
|
|
sessions.retain(|hash, _| *hash == keep_hash);
|
2026-03-21 01:21:08 +00:00
|
|
|
Self::save_to_disk(&sessions, &self.persist_path).await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Rotate a session: invalidate the old token and create a new one.
|
|
|
|
|
/// Returns the new plaintext token.
|
|
|
|
|
pub async fn rotate(&self, old_token: &str) -> String {
|
|
|
|
|
let old_hash = hash_token(old_token);
|
|
|
|
|
let new_token_bytes: [u8; 32] = rand::random();
|
|
|
|
|
let new_token = hex::encode(new_token_bytes);
|
|
|
|
|
let new_hash = hash_token(&new_token);
|
2026-03-15 04:32:24 +00:00
|
|
|
let now = SystemTime::now();
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
|
|
|
|
|
let mut sessions = self.sessions.write().await;
|
|
|
|
|
sessions.remove(&old_hash);
|
|
|
|
|
sessions.insert(
|
|
|
|
|
new_hash,
|
|
|
|
|
Session {
|
|
|
|
|
created_at: now,
|
|
|
|
|
last_activity: now,
|
|
|
|
|
session_type: SessionType::Full,
|
|
|
|
|
},
|
|
|
|
|
);
|
2026-03-21 01:21:08 +00:00
|
|
|
Self::save_to_disk(&sessions, &self.persist_path).await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
new_token
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Remove all expired sessions (cleanup).
|
2026-03-22 03:30:21 +00:00
|
|
|
#[cfg(test)]
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
pub async fn cleanup_expired(&self) {
|
|
|
|
|
let mut sessions = self.sessions.write().await;
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
sessions.retain(|_, session| match &session.session_type {
|
|
|
|
|
SessionType::Full => {
|
|
|
|
|
session
|
|
|
|
|
.last_activity
|
|
|
|
|
.elapsed()
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_secs()
|
|
|
|
|
< FULL_SESSION_TTL
|
|
|
|
|
}
|
|
|
|
|
SessionType::PendingTotp { .. } => {
|
|
|
|
|
session.created_at.elapsed().unwrap_or_default().as_secs() < PENDING_SESSION_TTL
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Evict the oldest full session if at or over the concurrent limit.
|
|
|
|
|
fn evict_if_over_limit(&self, sessions: &mut HashMap<[u8; 32], Session>) {
|
|
|
|
|
let full_count = sessions
|
|
|
|
|
.values()
|
|
|
|
|
.filter(|s| matches!(s.session_type, SessionType::Full))
|
|
|
|
|
.count();
|
|
|
|
|
|
|
|
|
|
if full_count >= MAX_CONCURRENT_SESSIONS {
|
|
|
|
|
// Find the oldest full session by last_activity
|
|
|
|
|
if let Some(oldest_hash) = sessions
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|(_, s)| matches!(s.session_type, SessionType::Full))
|
|
|
|
|
.min_by_key(|(_, s)| s.last_activity)
|
|
|
|
|
.map(|(h, _)| *h)
|
|
|
|
|
{
|
|
|
|
|
sessions.remove(&oldest_hash);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get the number of active full sessions.
|
2026-03-22 03:30:21 +00:00
|
|
|
#[cfg(test)]
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
pub async fn active_session_count(&self) -> usize {
|
|
|
|
|
let sessions = self.sessions.read().await;
|
|
|
|
|
sessions
|
|
|
|
|
.values()
|
|
|
|
|
.filter(|s| {
|
|
|
|
|
matches!(s.session_type, SessionType::Full)
|
2026-03-15 04:32:24 +00:00
|
|
|
&& s.last_activity.elapsed().unwrap_or_default().as_secs() < FULL_SESSION_TTL
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
})
|
|
|
|
|
.count()
|
|
|
|
|
}
|
2026-03-16 12:58:35 +00:00
|
|
|
|
|
|
|
|
// ── Remember-me token ──────────────────────────────────────────────
|
|
|
|
|
// HMAC-signed token that survives backend restarts. Secret is on disk.
|
|
|
|
|
// Format: "timestamp_hex:hmac_hex"
|
|
|
|
|
|
|
|
|
|
/// Create a remember-me token. Returns the cookie value.
|
2026-03-21 01:21:08 +00:00
|
|
|
pub async fn create_remember_token(&self) -> String {
|
|
|
|
|
let secret = Self::load_or_create_remember_secret().await;
|
2026-03-16 12:58:35 +00:00
|
|
|
let now = SystemTime::now()
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_secs();
|
|
|
|
|
let ts_hex = hex::encode(now.to_be_bytes());
|
|
|
|
|
let mut mac = HmacSha256::new_from_slice(&secret).expect("HMAC key");
|
|
|
|
|
mac.update(format!("remember:{}", ts_hex).as_bytes());
|
|
|
|
|
let sig = hex::encode(mac.finalize().into_bytes());
|
|
|
|
|
format!("{}:{}", ts_hex, sig)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Validate a remember-me token. Returns true if valid and not expired.
|
2026-03-21 01:21:08 +00:00
|
|
|
pub async fn validate_remember_token(token: &str) -> bool {
|
|
|
|
|
let secret = match tokio::fs::read(REMEMBER_SECRET_FILE).await {
|
2026-03-16 12:58:35 +00:00
|
|
|
Ok(s) if s.len() == 32 => s,
|
|
|
|
|
_ => return false,
|
|
|
|
|
};
|
|
|
|
|
let parts: Vec<&str> = token.splitn(2, ':').collect();
|
|
|
|
|
if parts.len() != 2 {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
let ts_hex = parts[0];
|
|
|
|
|
let sig_hex = parts[1];
|
|
|
|
|
|
|
|
|
|
// Verify HMAC
|
|
|
|
|
let mut mac = match HmacSha256::new_from_slice(&secret) {
|
|
|
|
|
Ok(m) => m,
|
|
|
|
|
Err(_) => return false,
|
|
|
|
|
};
|
|
|
|
|
mac.update(format!("remember:{}", ts_hex).as_bytes());
|
|
|
|
|
let expected_sig = match hex::decode(sig_hex) {
|
|
|
|
|
Ok(s) => s,
|
|
|
|
|
Err(_) => return false,
|
|
|
|
|
};
|
|
|
|
|
if mac.verify_slice(&expected_sig).is_err() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check expiry
|
|
|
|
|
let ts_bytes = match hex::decode(ts_hex) {
|
|
|
|
|
Ok(b) if b.len() == 8 => {
|
|
|
|
|
let mut arr = [0u8; 8];
|
|
|
|
|
arr.copy_from_slice(&b);
|
|
|
|
|
u64::from_be_bytes(arr)
|
|
|
|
|
}
|
|
|
|
|
_ => return false,
|
|
|
|
|
};
|
|
|
|
|
let now = SystemTime::now()
|
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.as_secs();
|
|
|
|
|
now.saturating_sub(ts_bytes) < REMEMBER_TTL
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-21 01:21:08 +00:00
|
|
|
pub async fn load_or_create_remember_secret() -> Vec<u8> {
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
REMEMBER_SECRET
|
|
|
|
|
.get_or_init(|| async {
|
|
|
|
|
// Try existing secret file first
|
|
|
|
|
if let Ok(secret) = tokio::fs::read(REMEMBER_SECRET_FILE).await {
|
|
|
|
|
if secret.len() == 32 {
|
|
|
|
|
return secret;
|
|
|
|
|
}
|
2026-03-31 11:06:19 +01:00
|
|
|
}
|
chore(ci): rustfmt + clippy clean-up to unblock the Rust CI job
The .github/workflows/ci.yml Rust job runs cargo fmt --check, clippy
with -D warnings, and tests. All three were failing. This commit:
- Applies rustfmt across the tree (the bulk of the diff — untouched
since the last toolchain bump, so a wide sweep was unavoidable).
- Fixes the correctness-level clippy errors:
container/bitcoin_simulator.rs wildcard-in-or-pattern
container/manifest.rs from_str rename to parse (reserved name)
container/podman_client.rs .get(0) -> .first()
container/runtime.rs manual += collapse
archipelago/src/constants.rs doc-comment → module-doc
api/rpc/package/install.rs stray /// comment above a non-item
container/docker_packages.rs redundant field init
streaming/advertisement.rs missing Metric import in tests
tests/orchestration_tests.rs `vec!` in non-Vec contexts
mesh/listener/dispatch.rs unused store_plain_message import
api/rpc/tor/mod.rs and mesh/steganography.rs: push-after-new → vec!
- Quiets wide legacy surfaces with crate-level allows in main.rs for
stylistic lints (too_many_arguments, type_complexity, doc indent,
enum variant prefix, wildcard-in-or, assertions-on-constants,
drop_non_drop, unused_io_amount, ptr_arg) — these fired in dozens
of places with no correctness payoff and have been churning every
toolchain bump.
- Tags intentional-dead-code helpers: wallet/ and streaming/ modules
are WIP, mesh::send_chunked_payload and DM_V1_MARKER are kept for
rollback compatibility, vpn::get_nostr_vpn_status is surface-area
for a not-yet-landed RPC.
cargo fmt --check, cargo clippy --all-targets --all-features
-- -D warnings, and cargo test --all-features now all pass locally.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:23:46 -04:00
|
|
|
// Generate a cryptographically random 32-byte secret on first boot
|
|
|
|
|
let mut secret = [0u8; 32];
|
|
|
|
|
rand::rngs::OsRng.fill_bytes(&mut secret);
|
|
|
|
|
// Ensure parent directory exists
|
|
|
|
|
if let Some(parent) = std::path::Path::new(REMEMBER_SECRET_FILE).parent() {
|
|
|
|
|
let _ = tokio::fs::create_dir_all(parent).await;
|
|
|
|
|
}
|
|
|
|
|
let _ = tokio::fs::write(REMEMBER_SECRET_FILE, &secret).await;
|
|
|
|
|
secret.to_vec()
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.clone()
|
2026-03-16 12:58:35 +00:00
|
|
|
}
|
2026-03-06 03:26:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn hash_token(token: &str) -> [u8; 32] {
|
|
|
|
|
let mut hasher = Sha256::new();
|
|
|
|
|
hasher.update(token.as_bytes());
|
|
|
|
|
hasher.finalize().into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Extract the session token from a Cookie header value.
|
|
|
|
|
pub fn extract_session_cookie(headers: &hyper::HeaderMap) -> Option<String> {
|
|
|
|
|
headers
|
|
|
|
|
.get("cookie")
|
|
|
|
|
.and_then(|v| v.to_str().ok())
|
|
|
|
|
.and_then(|cookies| {
|
|
|
|
|
cookies.split(';').find_map(|c| {
|
|
|
|
|
let c = c.trim();
|
|
|
|
|
c.strip_prefix("session=").map(|v| v.to_string())
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.filter(|v| !v.is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 23:54:14 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_session_create_and_validate() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
2026-03-10 23:54:14 +00:00
|
|
|
let token = store.create().await;
|
|
|
|
|
|
|
|
|
|
assert!(store.validate(&token).await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_session_invalid_token() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
2026-03-10 23:54:14 +00:00
|
|
|
assert!(!store.validate("nonexistent_token").await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_session_remove() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
2026-03-10 23:54:14 +00:00
|
|
|
let token = store.create().await;
|
|
|
|
|
|
|
|
|
|
assert!(store.validate(&token).await);
|
|
|
|
|
store.remove(&token).await;
|
|
|
|
|
assert!(!store.validate(&token).await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_pending_session_upgrade() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
2026-03-10 23:54:14 +00:00
|
|
|
let secret = vec![1, 2, 3, 4];
|
|
|
|
|
let token = store.create_pending(secret.clone()).await;
|
|
|
|
|
|
|
|
|
|
// Pending session should not validate as full
|
|
|
|
|
assert!(!store.validate(&token).await);
|
|
|
|
|
|
|
|
|
|
// Can get the TOTP secret
|
|
|
|
|
let got = store.get_pending_secret(&token).await;
|
|
|
|
|
assert_eq!(got, Some(secret));
|
|
|
|
|
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
// Upgrade to full — returns a new rotated token
|
|
|
|
|
let new_token = store.upgrade_to_full(&token).await;
|
|
|
|
|
assert!(new_token.is_some());
|
|
|
|
|
let new_token = new_token.unwrap();
|
|
|
|
|
|
|
|
|
|
// Old token should be invalid (rotated)
|
|
|
|
|
assert!(!store.validate(&token).await);
|
|
|
|
|
// New token should be valid
|
|
|
|
|
assert!(store.validate(&new_token).await);
|
2026-03-10 23:54:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_pending_session_max_attempts() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
2026-03-10 23:54:14 +00:00
|
|
|
let secret = vec![1, 2, 3];
|
|
|
|
|
let token = store.create_pending(secret).await;
|
|
|
|
|
|
|
|
|
|
// Exhaust MAX_TOTP_ATTEMPTS (5) + 1 to trigger removal
|
|
|
|
|
for _ in 0..MAX_TOTP_ATTEMPTS {
|
|
|
|
|
assert!(store.get_pending_secret(&token).await.is_some());
|
|
|
|
|
}
|
|
|
|
|
// 6th attempt should fail (session removed)
|
|
|
|
|
assert!(store.get_pending_secret(&token).await.is_none());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_extract_session_cookie() {
|
|
|
|
|
let mut headers = hyper::HeaderMap::new();
|
|
|
|
|
headers.insert("cookie", "session=abc123; other=xyz".parse().unwrap());
|
|
|
|
|
|
|
|
|
|
assert_eq!(extract_session_cookie(&headers), Some("abc123".to_string()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_extract_session_cookie_missing() {
|
|
|
|
|
let headers = hyper::HeaderMap::new();
|
|
|
|
|
assert_eq!(extract_session_cookie(&headers), None);
|
|
|
|
|
}
|
|
|
|
|
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_session_activity_updates_on_validate() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
let token = store.create().await;
|
|
|
|
|
|
|
|
|
|
// First validation should succeed and touch last_activity
|
|
|
|
|
assert!(store.validate(&token).await);
|
|
|
|
|
|
|
|
|
|
// Still valid after another validation
|
|
|
|
|
assert!(store.validate(&token).await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_invalidate_all_except() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
let token1 = store.create().await;
|
|
|
|
|
let token2 = store.create().await;
|
|
|
|
|
let token3 = store.create().await;
|
|
|
|
|
|
|
|
|
|
// Invalidate all except token2
|
|
|
|
|
store.invalidate_all_except(&token2).await;
|
|
|
|
|
|
|
|
|
|
assert!(!store.validate(&token1).await);
|
|
|
|
|
assert!(store.validate(&token2).await);
|
|
|
|
|
assert!(!store.validate(&token3).await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_session_rotate() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
let old_token = store.create().await;
|
|
|
|
|
|
|
|
|
|
assert!(store.validate(&old_token).await);
|
|
|
|
|
|
|
|
|
|
let new_token = store.rotate(&old_token).await;
|
|
|
|
|
|
|
|
|
|
// Old token should be invalid
|
|
|
|
|
assert!(!store.validate(&old_token).await);
|
|
|
|
|
// New token should be valid
|
|
|
|
|
assert!(store.validate(&new_token).await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_max_concurrent_sessions() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
let mut tokens = Vec::new();
|
|
|
|
|
|
|
|
|
|
// Create MAX_CONCURRENT_SESSIONS sessions
|
|
|
|
|
for _ in 0..MAX_CONCURRENT_SESSIONS {
|
|
|
|
|
tokens.push(store.create().await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All should be valid
|
|
|
|
|
for token in &tokens {
|
|
|
|
|
assert!(store.validate(token).await);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Creating one more should evict the oldest
|
|
|
|
|
let new_token = store.create().await;
|
|
|
|
|
assert!(store.validate(&new_token).await);
|
|
|
|
|
|
|
|
|
|
// The first token (oldest) should have been evicted
|
|
|
|
|
assert!(!store.validate(&tokens[0]).await);
|
|
|
|
|
|
|
|
|
|
// The rest should still be valid
|
|
|
|
|
for token in &tokens[1..] {
|
|
|
|
|
assert!(store.validate(token).await);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_active_session_count() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
assert_eq!(store.active_session_count().await, 0);
|
|
|
|
|
|
|
|
|
|
let token1 = store.create().await;
|
|
|
|
|
assert_eq!(store.active_session_count().await, 1);
|
|
|
|
|
|
|
|
|
|
let _token2 = store.create().await;
|
|
|
|
|
assert_eq!(store.active_session_count().await, 2);
|
|
|
|
|
|
|
|
|
|
store.remove(&token1).await;
|
|
|
|
|
assert_eq!(store.active_session_count().await, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_cleanup_expired_removes_stale() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
let token = store.create().await;
|
|
|
|
|
|
|
|
|
|
assert!(store.validate(&token).await);
|
|
|
|
|
assert_eq!(store.active_session_count().await, 1);
|
|
|
|
|
|
|
|
|
|
// Cleanup shouldn't remove active sessions
|
|
|
|
|
store.cleanup_expired().await;
|
|
|
|
|
assert_eq!(store.active_session_count().await, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_rotate_preserves_session_count() {
|
2026-03-31 01:41:24 +01:00
|
|
|
let store = SessionStore::new().await;
|
feat: add per-endpoint rate limiting for sensitive operations (PENTEST-04)
New EndpointRateLimiter in session.rs tracks requests per (method, IP)
with configurable limits and time windows:
Financial operations (5 req/5min):
- wallet.send, lnd.sendcoins, lnd.payinvoice, lnd.create-psbt,
lnd.finalize-psbt, wallet.ecash-send
Channel operations (3 req/5min):
- lnd.openchannel, lnd.closechannel
Backup operations (2-3 req/10min):
- backup.create, backup.restore
Container/package installs (5 req/5min):
- container-install, package.install
System operations (2 req/5min):
- system.reboot, system.shutdown, update.apply
Identity/auth (3-10 req/5min):
- identity.create, identity.issue-credential, auth.changePassword
Returns HTTP 429 with Retry-After header when limits exceeded.
Verified on live server: auth.changePassword blocks at 4th request,
lnd.sendcoins blocks at 6th request.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:46:25 +00:00
|
|
|
let token = store.create().await;
|
|
|
|
|
assert_eq!(store.active_session_count().await, 1);
|
|
|
|
|
|
|
|
|
|
let new_token = store.rotate(&token).await;
|
|
|
|
|
assert_eq!(store.active_session_count().await, 1);
|
|
|
|
|
assert!(store.validate(&new_token).await);
|
|
|
|
|
}
|
2026-03-10 23:54:14 +00:00
|
|
|
}
|