2026-03-22 03:30:21 +00:00
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
use tokio::fs;
|
|
|
|
|
|
|
|
|
|
use super::types::{CredentialStore, CREDENTIALS_DIR};
|
|
|
|
|
|
|
|
|
|
async fn ensure_dir(data_dir: &Path) -> Result<()> {
|
|
|
|
|
let dir = data_dir.join(CREDENTIALS_DIR);
|
|
|
|
|
if !dir.exists() {
|
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
|
|
|
fs::create_dir_all(&dir)
|
|
|
|
|
.await
|
|
|
|
|
.context("Creating credentials dir")?;
|
2026-03-22 03:30:21 +00:00
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn store_path(data_dir: &Path) -> std::path::PathBuf {
|
|
|
|
|
data_dir.join(CREDENTIALS_DIR).join("credentials.json")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn load_credentials(data_dir: &Path) -> Result<CredentialStore> {
|
|
|
|
|
ensure_dir(data_dir).await?;
|
|
|
|
|
let path = store_path(data_dir);
|
|
|
|
|
if !path.exists() {
|
|
|
|
|
return Ok(CredentialStore::default());
|
|
|
|
|
}
|
|
|
|
|
let raw = fs::read(&path).await.context("Reading credentials")?;
|
|
|
|
|
// Detect plaintext JSON (migration path) vs encrypted binary
|
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 raw.first().is_some_and(|b| *b == b'[' || *b == b'{') {
|
2026-03-22 03:30:21 +00:00
|
|
|
let data = String::from_utf8(raw).context("UTF-8 credentials")?;
|
|
|
|
|
return serde_json::from_str(&data).context("Parsing credentials");
|
|
|
|
|
}
|
|
|
|
|
// Encrypted: decrypt using node key
|
|
|
|
|
let key = load_encryption_key(data_dir).await?;
|
|
|
|
|
let plaintext = decrypt_credentials(&raw, &key)?;
|
|
|
|
|
serde_json::from_slice(&plaintext).context("Parsing decrypted credentials")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn save_credentials(data_dir: &Path, store: &CredentialStore) -> Result<()> {
|
|
|
|
|
ensure_dir(data_dir).await?;
|
|
|
|
|
let path = store_path(data_dir);
|
|
|
|
|
let data = serde_json::to_vec(store)?;
|
|
|
|
|
// Encrypt using node key
|
|
|
|
|
let key = load_encryption_key(data_dir).await?;
|
|
|
|
|
let encrypted = encrypt_credentials(&data, &key)?;
|
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
|
|
|
fs::write(&path, encrypted)
|
|
|
|
|
.await
|
|
|
|
|
.context("Writing credentials")
|
2026-03-22 03:30:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Derive a 32-byte encryption key from the node's identity key via SHA-256.
|
|
|
|
|
async fn load_encryption_key(data_dir: &Path) -> Result<[u8; 32]> {
|
|
|
|
|
let node_key_path = data_dir.join("identity").join("node_key");
|
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
|
|
|
let key_bytes = fs::read(&node_key_path)
|
|
|
|
|
.await
|
|
|
|
|
.context("Reading node key for credential encryption")?;
|
|
|
|
|
use sha2::{Digest, Sha256};
|
2026-03-22 03:30:21 +00:00
|
|
|
let mut hasher = Sha256::new();
|
|
|
|
|
hasher.update(b"archipelago-credential-store-v1");
|
|
|
|
|
hasher.update(&key_bytes);
|
|
|
|
|
let hash = hasher.finalize();
|
|
|
|
|
let mut key = [0u8; 32];
|
|
|
|
|
key.copy_from_slice(&hash);
|
|
|
|
|
Ok(key)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn encrypt_credentials(data: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
|
|
|
|
|
use chacha20poly1305::aead::{Aead, KeyInit};
|
|
|
|
|
let nonce_bytes: [u8; 12] = rand::random();
|
|
|
|
|
let cipher = chacha20poly1305::ChaCha20Poly1305::new_from_slice(key)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Cipher init: {}", e))?;
|
|
|
|
|
let ciphertext = cipher
|
|
|
|
|
.encrypt(
|
|
|
|
|
chacha20poly1305::aead::generic_array::GenericArray::from_slice(&nonce_bytes),
|
|
|
|
|
data,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Encryption failed: {}", e))?;
|
|
|
|
|
let mut output = Vec::with_capacity(12 + ciphertext.len());
|
|
|
|
|
output.extend_from_slice(&nonce_bytes);
|
|
|
|
|
output.extend_from_slice(&ciphertext);
|
|
|
|
|
Ok(output)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn decrypt_credentials(data: &[u8], key: &[u8; 32]) -> Result<Vec<u8>> {
|
|
|
|
|
use chacha20poly1305::aead::{Aead, KeyInit};
|
|
|
|
|
if data.len() < 12 {
|
|
|
|
|
anyhow::bail!("Encrypted credentials too short");
|
|
|
|
|
}
|
|
|
|
|
let nonce = &data[..12];
|
|
|
|
|
let ciphertext = &data[12..];
|
|
|
|
|
let cipher = chacha20poly1305::ChaCha20Poly1305::new_from_slice(key)
|
|
|
|
|
.map_err(|e| anyhow::anyhow!("Cipher init: {}", e))?;
|
|
|
|
|
cipher
|
|
|
|
|
.decrypt(
|
|
|
|
|
chacha20poly1305::aead::generic_array::GenericArray::from_slice(nonce),
|
|
|
|
|
ciphertext,
|
|
|
|
|
)
|
|
|
|
|
.map_err(|_| anyhow::anyhow!("Credential decryption failed — key mismatch or corruption"))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_load_credentials_returns_empty_when_no_file() {
|
|
|
|
|
let dir = tempfile::tempdir().unwrap();
|
|
|
|
|
let store = load_credentials(dir.path()).await.unwrap();
|
|
|
|
|
assert!(store.credentials.is_empty());
|
|
|
|
|
assert!(dir.path().join(CREDENTIALS_DIR).exists());
|
|
|
|
|
}
|
|
|
|
|
}
|