Skip to content

Commit 0f0baac

Browse files
committed
fix: derive encryption key from GitHub token instead of random per-machine key
Previous approach uploaded the encryption key alongside the encrypted data to the Gist, which defeats the purpose of encryption. Now the encryption key is deterministically derived from the GitHub OAuth token via Argon2 — same account produces the same key on every device, and nothing secret is stored in the cloud.
1 parent 4ffbdfc commit 0f0baac

3 files changed

Lines changed: 10 additions & 48 deletions

File tree

crates/hostsync-core/src/crypto.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,6 @@ pub fn decrypt(encoded: &str, passphrase: &str) -> Result<String, &'static str>
5757
String::from_utf8(plaintext).map_err(|_| "invalid utf8")
5858
}
5959

60-
/// Generates a random 64-char hex key for first-time setup.
61-
pub fn generate_random_key() -> String {
62-
let mut bytes = [0u8; 32];
63-
OsRng.fill_bytes(&mut bytes);
64-
hex::encode(&bytes)
65-
}
66-
67-
mod hex {
68-
pub fn encode(bytes: &[u8]) -> String {
69-
bytes.iter().map(|b| format!("{:02x}", b)).collect()
70-
}
71-
}
72-
7360
#[cfg(test)]
7461
mod tests {
7562
use super::*;

crates/hostsync-core/src/storage.rs

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ fn servers_path() -> PathBuf {
1212
data_dir().join("servers.enc")
1313
}
1414

15-
fn key_path() -> PathBuf {
16-
data_dir().join("enc.key")
17-
}
18-
1915
fn token_path() -> PathBuf {
2016
data_dir().join("github.json")
2117
}
@@ -25,23 +21,12 @@ fn ensure_dir() -> std::io::Result<()> {
2521
std::fs::create_dir_all(data_dir())
2622
}
2723

28-
/// Returns or generates the local encryption key.
24+
/// Derives the encryption key from the GitHub OAuth token.
25+
/// Same account → same token → same key on every device.
2926
pub fn get_encryption_key() -> String {
30-
if let Ok(key) = std::fs::read_to_string(key_path()) {
31-
if !key.trim().is_empty() {
32-
return key.trim().to_string();
33-
}
34-
}
35-
let key = crypto::generate_random_key();
36-
let _ = ensure_dir();
37-
let _ = std::fs::write(key_path(), &key);
38-
key
39-
}
40-
41-
/// Sets the encryption key (used when syncing key from cloud).
42-
pub fn set_encryption_key(key: &str) -> std::io::Result<()> {
43-
ensure_dir()?;
44-
std::fs::write(key_path(), key.trim())
27+
load_github_state()
28+
.token
29+
.unwrap_or_default()
4530
}
4631

4732
pub fn load_servers() -> Vec<Server> {

crates/hostsync-core/src/sync.rs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ use crate::storage;
22
use reqwest::Client;
33

44
const GIST_FILE_NAME: &str = "hostsync_data.enc";
5-
const GIST_KEY_FILE: &str = "hostsync_key.dat";
65
const GIST_DESCRIPTION: &str = "HostSync Encrypted Server Data";
76

87
fn headers(token: &str) -> reqwest::header::HeaderMap {
@@ -17,21 +16,17 @@ fn make_client() -> Result<Client, String> {
1716
crate::http::client()
1817
}
1918

20-
/// Uploads encrypted server data and encryption key to a GitHub Gist.
19+
/// Uploads encrypted server data to a GitHub Gist.
2120
pub async fn upload() -> Result<(), String> {
2221
let state = storage::load_github_state();
2322
let token = state.token.as_deref().ok_or("not logged in")?;
2423
let data = storage::get_raw_encrypted().ok_or("no data to upload")?;
25-
let enc_key = storage::get_encryption_key();
2624
let client = make_client()?;
2725

28-
let files = serde_json::json!({
29-
GIST_FILE_NAME: { "content": data },
30-
GIST_KEY_FILE: { "content": enc_key }
31-
});
32-
3326
if let Some(ref gist_id) = state.gist_id {
34-
let body = serde_json::json!({ "files": files });
27+
let body = serde_json::json!({
28+
"files": { GIST_FILE_NAME: { "content": data } }
29+
});
3530
let resp = client
3631
.patch(format!("https://api.github.com/gists/{}", gist_id))
3732
.headers(headers(token))
@@ -46,7 +41,7 @@ pub async fn upload() -> Result<(), String> {
4641
let body = serde_json::json!({
4742
"description": GIST_DESCRIPTION,
4843
"public": false,
49-
"files": files
44+
"files": { GIST_FILE_NAME: { "content": data } }
5045
});
5146
let resp = client
5247
.post("https://api.github.com/gists")
@@ -114,10 +109,5 @@ pub async fn download() -> Result<(), String> {
114109
.as_str()
115110
.ok_or("file not found in gist")?;
116111

117-
// Sync encryption key from cloud so other devices can decrypt
118-
if let Some(key) = gist["files"][GIST_KEY_FILE]["content"].as_str() {
119-
storage::set_encryption_key(key).map_err(|e| e.to_string())?;
120-
}
121-
122112
storage::set_raw_encrypted(content).map_err(|e| e.to_string())
123113
}

0 commit comments

Comments
 (0)