create-ekka-desktop-app 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -741,18 +741,9 @@ fn handle_ensure_node_identity(state: &EngineState) -> EngineResponse {
|
|
|
741
741
|
/// Requires local engine to be available (strict local engine mode).
|
|
742
742
|
/// Does NOT use Ed25519 register/challenge/session flow.
|
|
743
743
|
fn handle_bootstrap_node_session(payload: &Value, state: &EngineState) -> EngineResponse {
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
op = "node_runner.skipped.engine_unavailable",
|
|
748
|
-
engine_available = false,
|
|
749
|
-
"Node session bootstrap skipped: local engine not available"
|
|
750
|
-
);
|
|
751
|
-
return EngineResponse::err(
|
|
752
|
-
"ENGINE_UNAVAILABLE",
|
|
753
|
-
"Local engine not available. Node session requires local engine.",
|
|
754
|
-
);
|
|
755
|
-
}
|
|
744
|
+
// NodeSessionRunner is an in-process runner that uses node session auth (Authorization: Bearer).
|
|
745
|
+
// It does NOT require the local engine-bootstrap binary - it makes direct HTTP calls to the engine API.
|
|
746
|
+
// The engine_available check was overly restrictive and has been removed.
|
|
756
747
|
|
|
757
748
|
// Get home_path
|
|
758
749
|
let home_path = match state.home_path.lock() {
|
|
@@ -19,6 +19,7 @@ mod node_runner;
|
|
|
19
19
|
mod node_vault_crypto;
|
|
20
20
|
mod node_vault_store;
|
|
21
21
|
mod ops;
|
|
22
|
+
mod security_epoch;
|
|
22
23
|
mod state;
|
|
23
24
|
mod types;
|
|
24
25
|
|
|
@@ -31,7 +32,8 @@ use tauri::Manager;
|
|
|
31
32
|
|
|
32
33
|
fn main() {
|
|
33
34
|
// Load .env.local for development (before anything else)
|
|
34
|
-
// This provides
|
|
35
|
+
// This provides optional dev-time overrides (EKKA_SECURITY_EPOCH, etc).
|
|
36
|
+
// Note: EKKA_SECURITY_EPOCH is optional - epoch is resolved from marker file by default.
|
|
35
37
|
// Note: ENGINE_GRANT_VERIFY_KEY_B64 is now fetched from /.well-known/ekka-configuration
|
|
36
38
|
if let Err(_) = dotenvy::from_filename(".env.local") {
|
|
37
39
|
// Also try parent directory (when running from src-tauri)
|
|
@@ -63,14 +65,6 @@ fn main() {
|
|
|
63
65
|
// Attempt to spawn engine process
|
|
64
66
|
tracing::info!(op = "desktop.startup", "EKKA Desktop starting");
|
|
65
67
|
|
|
66
|
-
// Log security epoch status (still needed for home bootstrap)
|
|
67
|
-
let security_epoch_set = std::env::var("EKKA_SECURITY_EPOCH").is_ok();
|
|
68
|
-
tracing::info!(
|
|
69
|
-
op = "desktop.required_env.loaded",
|
|
70
|
-
EKKA_SECURITY_EPOCH = security_epoch_set,
|
|
71
|
-
"Security epoch env var status"
|
|
72
|
-
);
|
|
73
|
-
|
|
74
68
|
// Log build-time baked engine URL presence (not the URL itself)
|
|
75
69
|
let engine_url_baked = option_env!("EKKA_ENGINE_URL").is_some();
|
|
76
70
|
tracing::info!(
|
|
@@ -16,6 +16,7 @@ use crate::node_vault_store::{
|
|
|
16
16
|
delete_node_secret, has_node_secret, read_node_secret, write_node_secret,
|
|
17
17
|
SECRET_ID_NODE_CREDENTIALS,
|
|
18
18
|
};
|
|
19
|
+
use crate::security_epoch::resolve_security_epoch;
|
|
19
20
|
use chrono::{DateTime, Utc};
|
|
20
21
|
use serde::{Deserialize, Serialize};
|
|
21
22
|
use std::sync::RwLock;
|
|
@@ -134,17 +135,6 @@ impl std::fmt::Display for CredentialsError {
|
|
|
134
135
|
|
|
135
136
|
impl std::error::Error for CredentialsError {}
|
|
136
137
|
|
|
137
|
-
// =============================================================================
|
|
138
|
-
// Helper: Get epoch for vault operations
|
|
139
|
-
// =============================================================================
|
|
140
|
-
|
|
141
|
-
/// Get the security epoch from environment
|
|
142
|
-
fn get_security_epoch() -> u32 {
|
|
143
|
-
std::env::var("EKKA_SECURITY_EPOCH")
|
|
144
|
-
.ok()
|
|
145
|
-
.and_then(|s| s.parse().ok())
|
|
146
|
-
.unwrap_or(1)
|
|
147
|
-
}
|
|
148
138
|
|
|
149
139
|
// =============================================================================
|
|
150
140
|
// Core Functions
|
|
@@ -170,7 +160,7 @@ pub fn store_credentials(node_id: &Uuid, node_secret: &str) -> Result<(), Creden
|
|
|
170
160
|
// Initialize home if needed
|
|
171
161
|
let bootstrap = initialize_home().map_err(|e| CredentialsError::VaultError(e))?;
|
|
172
162
|
let home = bootstrap.home_path();
|
|
173
|
-
let epoch =
|
|
163
|
+
let epoch = resolve_security_epoch(home);
|
|
174
164
|
|
|
175
165
|
// Store node_id + node_secret as JSON
|
|
176
166
|
let creds = NodeCredentials {
|
|
@@ -202,7 +192,7 @@ pub fn store_credentials(node_id: &Uuid, node_secret: &str) -> Result<(), Creden
|
|
|
202
192
|
/// * `Err(CredentialsError)` if not found or invalid
|
|
203
193
|
pub fn load_credentials() -> Result<(Uuid, String), CredentialsError> {
|
|
204
194
|
let home = resolve_home_path().map_err(|e| CredentialsError::VaultError(e))?;
|
|
205
|
-
let epoch =
|
|
195
|
+
let epoch = resolve_security_epoch(&home);
|
|
206
196
|
|
|
207
197
|
// Key derivation uses device_secret + epoch only (not node_id)
|
|
208
198
|
let plaintext = read_node_secret(&home, epoch, SECRET_ID_NODE_CREDENTIALS)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
//! Security Epoch Resolution
|
|
2
|
+
//!
|
|
3
|
+
//! Resolves the security epoch used for vault key derivation.
|
|
4
|
+
//! Resolution order:
|
|
5
|
+
//! 1. EKKA_SECURITY_EPOCH env var (override for dev/testing)
|
|
6
|
+
//! 2. Marker file epoch_seen field (canonical source)
|
|
7
|
+
//! 3. Default to 1 (new installations)
|
|
8
|
+
|
|
9
|
+
use std::path::Path;
|
|
10
|
+
use std::sync::OnceLock;
|
|
11
|
+
|
|
12
|
+
/// Cached resolved epoch (env var source only - marker is read fresh)
|
|
13
|
+
static ENV_EPOCH_CACHE: OnceLock<Option<u32>> = OnceLock::new();
|
|
14
|
+
|
|
15
|
+
/// Source of the resolved epoch
|
|
16
|
+
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
17
|
+
pub enum EpochSource {
|
|
18
|
+
Env,
|
|
19
|
+
Marker,
|
|
20
|
+
Default,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl std::fmt::Display for EpochSource {
|
|
24
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
25
|
+
match self {
|
|
26
|
+
EpochSource::Env => write!(f, "env"),
|
|
27
|
+
EpochSource::Marker => write!(f, "marker"),
|
|
28
|
+
EpochSource::Default => write!(f, "default"),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Resolve security epoch from env var
|
|
34
|
+
fn try_env_epoch() -> Option<u32> {
|
|
35
|
+
*ENV_EPOCH_CACHE.get_or_init(|| {
|
|
36
|
+
std::env::var("EKKA_SECURITY_EPOCH")
|
|
37
|
+
.ok()
|
|
38
|
+
.and_then(|s| s.parse().ok())
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Read epoch_seen from marker file
|
|
43
|
+
fn try_marker_epoch(home: &Path) -> Option<u32> {
|
|
44
|
+
let marker_path = home.join(".ekka-marker.json");
|
|
45
|
+
let content = std::fs::read_to_string(&marker_path).ok()?;
|
|
46
|
+
let marker: serde_json::Value = serde_json::from_str(&content).ok()?;
|
|
47
|
+
marker.get("epoch_seen").and_then(|v| v.as_u64()).map(|v| v as u32)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Resolve security epoch with precedence: env > marker > default
|
|
51
|
+
///
|
|
52
|
+
/// Logs the resolution once per unique (home, source) combination.
|
|
53
|
+
pub fn resolve_security_epoch(home: &Path) -> u32 {
|
|
54
|
+
let (epoch, source) = resolve_with_source(home);
|
|
55
|
+
|
|
56
|
+
// Log resolution (only first time per call site in practice)
|
|
57
|
+
tracing::info!(
|
|
58
|
+
op = "security_epoch.resolved",
|
|
59
|
+
source = %source,
|
|
60
|
+
value = epoch,
|
|
61
|
+
"Security epoch resolved"
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
epoch
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Resolve epoch and return both value and source
|
|
68
|
+
pub fn resolve_with_source(home: &Path) -> (u32, EpochSource) {
|
|
69
|
+
// 1. Check env var override
|
|
70
|
+
if let Some(epoch) = try_env_epoch() {
|
|
71
|
+
return (epoch, EpochSource::Env);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Read from marker file
|
|
75
|
+
if let Some(epoch) = try_marker_epoch(home) {
|
|
76
|
+
return (epoch, EpochSource::Marker);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 3. Default
|
|
80
|
+
(1, EpochSource::Default)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[cfg(test)]
|
|
84
|
+
mod tests {
|
|
85
|
+
use super::*;
|
|
86
|
+
use tempfile::TempDir;
|
|
87
|
+
use std::fs;
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn test_marker_epoch_read() {
|
|
91
|
+
let temp = TempDir::new().unwrap();
|
|
92
|
+
let marker = serde_json::json!({
|
|
93
|
+
"schema_version": "1.0",
|
|
94
|
+
"app_name": "test",
|
|
95
|
+
"instance_id": "00000000-0000-0000-0000-000000000000",
|
|
96
|
+
"device_id_fingerprint": "sha256:test",
|
|
97
|
+
"created_at": "2024-01-01T00:00:00Z",
|
|
98
|
+
"last_seen_at": "2024-01-01T00:00:00Z",
|
|
99
|
+
"epoch_seen": 42,
|
|
100
|
+
"storage_layout_version": "v1"
|
|
101
|
+
});
|
|
102
|
+
fs::write(temp.path().join(".ekka-marker.json"), marker.to_string()).unwrap();
|
|
103
|
+
|
|
104
|
+
let epoch = try_marker_epoch(temp.path());
|
|
105
|
+
assert_eq!(epoch, Some(42));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn test_default_when_no_marker() {
|
|
110
|
+
let temp = TempDir::new().unwrap();
|
|
111
|
+
let (epoch, source) = resolve_with_source(temp.path());
|
|
112
|
+
assert_eq!(epoch, 1);
|
|
113
|
+
assert_eq!(source, EpochSource::Default);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[test]
|
|
117
|
+
fn test_marker_source() {
|
|
118
|
+
let temp = TempDir::new().unwrap();
|
|
119
|
+
let marker = serde_json::json!({
|
|
120
|
+
"epoch_seen": 5
|
|
121
|
+
});
|
|
122
|
+
fs::write(temp.path().join(".ekka-marker.json"), marker.to_string()).unwrap();
|
|
123
|
+
|
|
124
|
+
let (epoch, source) = resolve_with_source(temp.path());
|
|
125
|
+
assert_eq!(epoch, 5);
|
|
126
|
+
assert_eq!(source, EpochSource::Marker);
|
|
127
|
+
}
|
|
128
|
+
}
|