Psync
- 45 Devlogs
- 93 Total hours
Psync is a lazy project archiver designed to make storing files on a nas easier than dealing with automountd or fuse
Psync is a lazy project archiver designed to make storing files on a nas easier than dealing with automountd or fuse
Add bash tab completion script for pwr CLI commands
Provide a bash completion function for all pwr subcommands and their
flags. The completion script maps each subcommand to its valid arguments:
init completes –server-host, –server-port, –psk, and –local-root;
archive, restore, and ensure complete directory paths; status and list
offer –recursive/-r; log provides –errors/-e and –project; shell
completes bash, zsh, fish, and –init. The completion is installed via
‘source <(pwr completions bash)’ in .bashrc.
Document required parentheses around impl Trait syntax in handler signatures
The Rust 2024 edition requires explicit parentheses around compound trait
bounds in function argument position. Without them, ‘&mut impl Read + Write’
is ambiguous: it could parse as ‘(&mut impl Read) + Write’ (a mutable
reference to a type implementing Read, with an additional Write bound) or
‘&mut (impl Read + Write)’ (a mutable reference to a type implementing
both). The compiler requires the latter to be written with parentheses.
Add a module-level doc comment explaining this so future maintainers do
not attempt to remove the parentheses.
Add end-to-end integration tests for full client-server lifecycle
Implement two end-to-end tests using paired in-memory byte streams (Pipe
struct implementing Read+Write via Arc<Mutex<Vec») that simulate a
complete client-server interaction without network dependencies.
test_full_client_server_archive_restore_cycle creates a multi-file project
directory, generates age encryption keys and a PSK, runs the archive
pipeline to produce an encrypted blob, then spawns two threads for client
and server communicating through paired pipes. The client performs the
full protocol sequence: Handshake with HMAC-SHA256 proof, ArchiveRequest
with project metadata, chunked data transfer with 1 MiB chunks and
zero-length EOF marker, and ArchiveComplete with SHA-256 hash. The server
thread receives the Handshake, verifies the client proof, responds with
its own server proof for mutual authentication, reads the ArchiveRequest,
receives all chunks via the 4-byte length-prefixed streaming protocol, and
acknowledges ArchiveComplete. After both threads join, assertions verify
the server received the exact encrypted blob, decryption succeeds, and the
local project metadata transitions correctly through local → archived →
placeholder states with .project.toml file survival.
test_end_to_end_hash_verification_prevents_corruption validates that
tampering with a single byte of the encrypted archive produces a different
SHA-256 hash, ensuring integrity violations are detectable before
extraction.
Add operation progress overlay to TUI and wire pwr tui subcommand in CLI
Implement an OperationOverlay screen for the ratatui-based TUI that renders
a centered modal dialog with a progress gauge, transfer statistics, and a
cancel prompt during archive and restore operations. The overlay displays
the operation name and project name in the title bar, a cyan progress
gauge driven by the bytes_done/bytes_total ratio, a statistics line
showing human-readable byte counts with percentage, and a status line that
changes between ‘Press Esc to cancel’ during transfer, ‘Transfer complete’
on success, and red error text on failure. The format_size helper converts
byte counts to GB/MB/KB/B with one decimal place. The handle_input method
maps Escape to cancellation and Enter to dismissal after completion.
Wire the pwr tui subcommand into the CLI behind the tui feature flag. The
Commands enum gains a Tui variant gated by #[cfg(feature = “tui”)], the
dispatch match arm calls cmd_tui(), and the tui module is conditionally
compiled. Building without –features tui produces a CLI-only binary;
building with the flag includes the ratatui and crossterm dependencies and
the tui module, enabling pwr tui to launch the interactive interface.
Annotate session state structs with allow(dead_code) for future use
The ArchiveSession and RestoreSession structs in the handler state machine
contain fields (session_id, file_count, compression, bytes_received,
bytes_sent) that are stored for future use in resume-after-interruption
logic and detailed transfer statistics. Currently they are only written
during state transitions but not yet read back. Add #[allow(dead_code)]
annotations to suppress compiler warnings while keeping the fields
available for the planned transfer progress tracking and interrupted
archive resume features.
Fix warning: remove unused variable and clean up imports
Remove the unused config variable clone from the listener accept loop where
it was captured by the thread closure but never referenced. Remove the
unused PrivateKeyDer import from rustls pki_types. Prefix the catch-all
match variable with underscore to suppress the unused variable warning in
the dispatch function. Keep the required parentheses around impl Trait
syntax in argument position as required by Rust 2024 edition for
disambiguation. Add MIT LICENSE file.
Add systemd service unit and deployment guides for server and client
Create a hardened systemd service unit for pwr-server with security
directives including NoNewPrivileges, PrivateTmp, ProtectSystem=strict,
ProtectHome=yes, and read-only access to config and certificate paths
with read-write access only to the project storage directory. The service
runs as the pwr system user, restarts on failure with a 5-second delay,
and logs to journald. Resource limits cap file descriptors at 4096 and
memory at 512 MB.
Add docs/server-setup.md covering the complete Debian 12 deployment
workflow: installing build dependencies, compiling with cargo,
initializing the server to generate TLS certificates and PSK, creating the
storage directory with correct ownership, installing and enabling the
systemd service, verifying the listener with ss, configuring firewall
rules for nftables and iptables, and troubleshooting common issues.
Add docs/client-setup.md covering the Arch Linux client workflow:
building the binary, running pwr init with server host and PSK, setting
up shell integration for bash, zsh, and fish, performing the first archive
and restore, launching the TUI, and troubleshooting configuration and
authentication errors. Include a full configuration file reference.
Fix client integration tests and add retry-aware connect test
Repair the client integration test suite after the retry logic and library
target additions. The connect_to_nonexistent_server test now uses a match
statement instead of unwrap_err to work with the ClientResult type.
The connect_timeout_is_respected test is marked #[ignore] because OS-level
TCP timeout behavior varies across platforms, making sub-second timeout
assertions unreliable in CI environments. Unused imports (TcpListener,
Duration) are removed from the test file.
Add progress bar integration to CLI with empty-archive edge case handling also add 0-byte transfer file size.
Add progress callback support to client archive and restore methods
Extend archive_project and restore_project with optional progress callbacks
that report bytes transferred against total size. The existing methods
become thin wrappers calling the new archive_project_with_progress and
restore_project_with_progress variants with None for backward
compatibility. During archive, the callback fires after each 1 MiB chunk
is written to the TCP stream, reporting cumulative bytes sent. During
restore, the callback fires after each received chunk is appended to the
buffer, reporting cumulative bytes received. The callback signature is
fn(u64, u64) where the first argument is current progress and the second
is the total, suitable for driving indicatif progress bars or TUI
progress displays without coupling the client module to any specific UI
library.
Add exponential backoff retry logic to PwrClient connection
Implement connect_with_retry in PwrClient that wraps the existing connection
logic with up to 3 retry attempts using exponential backoff starting at 1
second and doubling to 2 and 4 seconds. The existing connect method is
renamed to connect_once and made private, with the public connect method
now calling it in a retry loop. Transient network failures such as TCP
connection refused or TLS handshake timeouts are retried, while
authentication failures are not (they propagate immediately since retrying
with the same credentials would produce the same result). Each retry
attempt is logged at WARN level with the error and delay, and a successful
retry is logged at INFO level. The connect_timeout_secs from the client
config is applied to each individual TCP connection attempt within the
retry loop.
Implement TLS transport with custom certificate verifier and fingerprint pinning
Replace the TLS-not-yet-implemented stub in the PwrClient with a working
rustls-based TLS 1.3 connection layer. The connect_tls function builds a
ClientConfig using the dangerous API with a custom certificate verifier
(CertVerifier) that accepts self-signed server certificates — necessary
because pwr-server generates its own ECDSA P-256 certificate during init
rather than obtaining one from a public CA. Authentication is provided by
the PSK handshake running over the encrypted channel, so the TLS layer
provides confidentiality while the application layer handles mutual
authentication.
The verifier supports optional SHA-256 certificate fingerprint pinning.
When a server_fingerprint is present in the client config, the verifier
computes the SHA-256 hash of the server’s end-entity certificate and
compares it against the pinned value, rejecting the connection on
mismatch. When no fingerprint is configured, the verifier accepts any
certificate but logs the observed fingerprint for the user to pin later,
enabling a trust-on-first-use workflow.
Signature verification in the verifier accepts all TLS 1.2 and 1.3
signatures without checking against a root CA. This is intentional: the
security model relies on PSK-based mutual authentication at the
application layer combined with certificate pinning for defense-in-depth,
rather than on the Web PKI trust infrastructure which would reject
self-signed certificates. The supported_verify_schemes list advertises
ECDSA and RSA schemes to maximize compatibility.
Add server address display to status command output
Extend the status command to print the configured server host and port
after the project count summary, giving users a quick reminder of which
NAS their projects are archived to.
Fix server handler and listener warnings: remove unnecessary parentheses and unused imports
Clean up compilation warnings in the pwr-server crate. In handler.rs, the
send_server_msg function signature used unnecessary parentheses around
impl Write which triggered the unused_parens lint. In listener.rs, the
PrivateKeyDer import was unused since the key is loaded via
rustls_pemfile::private_key which returns a generic PrivateKeyDer type
that does not need to be explicitly imported.
Clean up unused imports and fix compilation warnings in client TLS module
Remove unused Arc import from the top-level client scope (it is only
needed inside the connect_tls function where it is imported locally).
Replace TrustAnchor with the correct ServerName import from
rustls::pki_types. Remove the unnecessary mut qualifier from the
ClientConnection binding in connect_tls.
Add –tls flag to all network commands, –limit to log, –auto-install to shell init
Extend the CLI with connection and usability flags across all subcommands.
Every command that contacts the server (archive, restore, ensure, status,
list) now accepts a –tls flag that is passed through to
PwrClient::connect, controlling whether the connection uses TLS with
certificate pinning or plaintext for local testing. The log command gains
a –limit flag defaulting to 50 entries, with a summary line showing how
many entries were truncated. The shell init command supports
–auto-install which opens the shell rc file in append mode and writes
the eval line directly, providing a one-command setup experience.
Function signatures throughout main.rs are updated to thread the new
boolean parameter. The ensure command passes tls=false through to its
internal restore call since ensure is triggered from the shell wrapper
where TLS may not be configured. The list command delegates to
cmd_status with tls=false for local-only directory scanning.
Wire progress bars and retry logic into CLI archive and restore commands
Integrate the progress reporting module and retry logic into the two main
CLI workflows. The archive command now creates a progress bar before the
packaging pipeline and passes it to create_archive_with_progress via a
ProgressFn callback that maps ArchiveStage values to status messages and
fractional bar position. A second progress bar tracks the upload phase.
Both bars are finished and cleared on completion. The restore command
similarly uses a download progress bar keyed to the stored project size
and an extraction progress bar sized to the received encrypted blob.
Connection attempts now use with_retry wrapping PwrClient::connect with 3
retries and a 1-second base delay, retrying only on transient network
errors as classified by is_retryable_error. This provides resilience
against momentary server unavailability without masking fatal errors
like authentication failures.
Add client integration test suite with handshake, streaming, retry, and progress tests
Create 14 integration tests in pwr-cli/tests/client_integration.rs covering
the client-side protocol layer. Handshake tests verify that ClientMessage
frames encode with correct magic bytes, version, and type discriminants,
and that decode_client_message recovers the original Handshake struct with
client_id and nonce intact. Archive flow tests exercise ArchiveRequest and
ArchiveComplete serialization round-trips with all fields including UUID,
name, total_size, file_count, compression, and hash. Restore flow tests
verify RestoreRequest encoding and decoding.
Server response parsing tests validate that HandshakeAck, Error, and
StatusResponse messages are correctly deserialized from framed payloads,
including success flags, error codes, and project info vectors with UUID
and file_count fields. Chunk streaming tests cover five size boundaries
from zero bytes (EOF detection) through 1 MiB, plus standalone EOF marker
encoding. Retry logic tests verify is_retryable_error correctly classifies
connection failures, timeouts, and broken pipes as transient while treating
authentication and not-found errors as fatal. with_retry is tested for
immediate success, maximum retry exhaustion (initial + 3 retries = 4
calls), and early termination on non-retryable errors after 1 call.
A progress stage test confirms all five ArchiveStage variants are distinct.
Add progress reporting module with indicatif integration and stage callbacks
Introduce a progress module in the pwr CLI crate that provides two
display modes for long-running operations. The archive_progress_bar and
restore_progress_bar functions create indicatif ProgressBar instances
with styled templates showing a spinner, progress bar, byte counter,
and ETA. The update_archive_progress function maps ArchiveStage enum
values to human-readable status messages displayed in the bar.
For non-interactive terminals or quiet mode, print_stage emits a simple
line to stderr for each stage transition during archive or extract
operations. This provides visibility into long-running operations
without requiring a TTY.
Implement TLS connector with certificate pinning and retry logic for the client
Add a complete TLS connection path to PwrClient, replacing the previous
stub that returned a not-yet-implemented error. The connect_tls function
builds a rustls ClientConfig with webpki root certificates for standard
PKI validation. When a server_fingerprint is set in the client config, a
custom FingerprintVerifier replaces the default verifier. This verifier
computes the SHA-256 hash of the server’s end-entity certificate DER
bytes and compares it against the pinned hex fingerprint, implementing
trust-on-first-use style certificate pinning without requiring a private
CA. The verifier supports TLS 1.2 and 1.3 signature schemes including
RSA and ECDSA P-256.
The PwrClient struct gains server_addr, psk, and pinned_fingerprint
fields, enabling a reconnect method that re-establishes a TCP connection
to the same server and re-runs the PSK handshake without reloading the
config file. The connect method now branches on use_tls, routing to
either the TLS path with certificate validation or the plaintext path
for local testing.
Add retry logic via the with_retry helper, a generic function that wraps
any fallible operation with exponential backoff. The caller provides a
retryable-error predicate; is_retryable_error recognizes connection
refused, timeout, connection closed, connection reset, broken pipe, and
unexpected EOF as transient errors worth retrying. The base delay
doubles each attempt, with a configurable maximum number of retries.
Add server integration test suite and library target for external testing
Create 17 integration tests in pwr-server/tests/server_integration.rs
covering the full server subsystem. Storage CRUD tests (6) verify project
creation with duplicate rejection, removal, archive read/write round-trips
through the storage layer, persistence across re-opens via registry JSON,
and size limit enforcement rejecting archives exceeding the configured
max_project_size_gb. PSK handshake tests (3) validate the full mutual
authentication flow with correct proof verification and wrong-PSK
rejection. Frame-level integration tests (3) exercise the complete
serialization pipeline: Handshake framing round-trip with client_id and
nonce preservation, ArchiveRequest framing with all fields intact, and
server message decode direction checking.
File chunk streaming tests (2) simulate the chunk protocol by encoding
250 KB of cyclical data across 4 KB chunks with 4-byte length prefixes
and EOF markers, reassembling on the receiver side and verifying content
equality. An empty archive test confirms that a standalone EOF marker
produces zero bytes of received data. Rate limiter tests (3) verify the
6th attempt is blocked after 5 allowed, success resets the window, and
different IPs are tracked independently.
A full archive-restore simulation test writes 500 KB of data through the
storage layer, reads it back, and verifies byte-exact restoration plus
metadata persistence. A cleanup test confirms that removing a project
after a failed archive deletes both the archive file and registry entry.
To support external integration tests, a library target (lib.rs) is added
to pwr-server, re-exporting all modules as public while the binary target
(main.rs) retains its own private module declarations.
Add file chunk streaming to archive and restore handlers
Implement the raw chunk streaming protocol in both directions on the server
side. After accepting an ArchiveRequest, the handler enters a chunk receive
loop (handle_archive_chunks) that reads 4-byte big-endian length prefixes
followed by that many bytes of data, appending each chunk to the project’s
archive file on disk via append-mode file I/O. A zero-length chunk
signals EOF. The total bytes received is logged for monitoring.
On the restore side, handle_restore_chunks reads the complete archive file
from disk through the storage layer, then streams it back to the client in
1 MiB chunks with the same 4-byte length-prefixed format, followed by a
zero-length EOF marker and a flush. This mirrors the client’s
restore_project method which reads chunks in the same format.
The dispatch function now chains the chunk streaming phase after the
initial request acceptance: ArchiveRequest dispatch calls
handle_archive_start followed by handle_archive_chunks, and
RestoreRequest calls handle_restore_start followed by
handle_restore_chunks. The ArchiveComplete and RestoreComplete messages
are received as framed messages after the chunk phase completes, handled
by the existing finish functions.
Add IP-based authentication rate limiter and integrate into handshake handler
Implement a per-IP rate limiter in the server’s auth module that tracks
failed authentication attempts within a sliding 60-second window. Each IP
is allowed up to 5 failed handshake attempts before being banned for 300
seconds. The RateLimiter maintains an in-memory HashMap<IpAddr,
RateLimitEntry> with periodic cleanup of expired entries to prevent
unbounded memory growth.
The check_attempt method atomically increments the attempt counter, resets
the window if it has expired, checks whether the IP is currently banned,
and returns false if the attempt should be rejected. record_success resets
the counter and clears any ban on successful authentication, allowing
legitimate clients that temporarily misconfigured their PSK to recover.
The rate limiter is shared across all connections via Arc<Mutex<» and
passed through HandlerContext. The handshake handler calls check_attempt
before validating the PSK proof, ensuring brute-force attempts are blocked
before any cryptographic work is performed. If the rate limit is exceeded,
a HandshakeAck with a descriptive error is sent and the connection is
closed. On successful authentication, record_success is called to clear
the counter.
Four unit tests verify that the first five attempts are allowed, the sixth
is blocked, record_success resets the counter, and different IPs are
tracked independently.
Implement server main entry point with clap CLI and fix listener/handler compilation
Add a complete main.rs for pwr-server with three clap subcommands. The
init subcommand detects or accepts a hostname, delegates to cert::init_server
for TLS certificate generation, PSK creation, and config file writing, then
prints the start command. The start subcommand locates the config file via
the standard search path, loads and validates the ServerConfig, logs the
bind address and storage path via tracing, and calls listener::run to begin
accepting connections (blocks until shutdown). The status subcommand loads
the config and prints a summary of all settings plus a connectivity check
by attempting a brief TCP connection to the configured port, reporting
whether the server appears to be running.
Logging is configured via tracing-subscriber with an env-filter defaulting
to pwr_server=info, controllable via the RUST_LOG environment variable.
The –foreground flag on the start subcommand is accepted but daemonization
is not yet implemented.
Fix two compilation errors discovered during integration. The listener’s
TLS setup incorrectly pattern-matched rustls::StreamOwned::new as if it
returned a Result; it returns the stream directly in rustls 0.23.
The handler’s handle_archive_finish had a borrow conflict where the
session reference from the Archiving state match arm prevented reassigning
*state; the fix extracts project_uuid and project_name into local
variables before the state transition.
Add comprehensive crypto integration test suite with 26 tests
Introduce crypto_integration.rs exercising the full security subsystem
across four categories. PSK tests (8) verify 256-bit key generation
across ten iterations, hex round-trip preservation including case
insensitivity, rejection of odd-length and wrong-length hex strings,
deterministic client proof computation, the complete mutual
authentication flow with distinct client and server proofs, and PSK
binding where different keys produce different proofs.
Age encryption tests (6) cover small, empty, and 1 MB large payload
round-trips, ephemeral key non-determinism (same plaintext produces
different ciphertext but both decrypt correctly), wrong identity
rejection, and corrupted ciphertext detection via single-byte
flipping mid-payload.
HKDF tests (4) verify 256-bit output length, deterministic derivation
for same PSK and UUID, cross-project key uniqueness for different UUIDs,
and a known-UUID stability test confirming the derived key is neither
all-zeros nor all-PSK-bytes.
Integrity tests (3) confirm streaming hasher equivalence with oneshot
hashing across 5 MB of 64 KB chunks, empty file hashing matching the
well-known empty SHA-256 digest, and proper I/O error propagation for
nonexistent files.
Archive round-trip tests (3) create a multi-file project with README,
two Rust sources, a design doc, and Cargo.toml, run through
create_archive → extract_archive, verify all file contents match
exactly and .project.toml is excluded. Hash verification rejection
confirms wrong hashes prevent extraction. A progress callback test
records all stage transitions during archive creation and verifies the
final progress fraction reaches 1.0.
Add progress callback support to archive and extract pipelines
Introduce progress reporting to the archive packaging and extraction
functions through an optional callback parameter. The ProgressFn type is a
boxed closure receiving an ArchiveStage enum and a fractional progress
value from 0.0 to 1.0. Eight stages cover the full lifecycle: Scanning,
Tarring, Compressing, Encrypting, Hashing, Decrypting, Decompressing, and
Extracting.
The original create_archive and extract_archive signatures are preserved
as convenience wrappers that call the new create_archive_with_progress and
extract_archive_with_progress variants with None for the callback,
maintaining backward compatibility for callers that do not need progress
reporting.
Progress granularity is coarse but meaningful: the archive pipeline
reports six transitions (Scanning at 0.0, Tarring at 0.1, Compressing at
0.3, Encrypting at 0.5, Hashing at 0.9 and 1.0). The extract pipeline
reports eight transitions (Hashing at 0.0 and 0.2, Decrypting at 0.3 and
0.5, Decompressing at 0.6, Extracting at 0.7 and 1.0). Future
refinements could report per-file progress during the tar stage by
tracking bytes written against the pre-computed directory size.
Add HKDF-SHA256 per-project key derivation from master PSK
Implement derive_project_key in the crypto module using ring’s HKDF
implementation with SHA-256. The function takes the 256-bit master PSK as
the HKDF salt and the project UUID bytes as the info parameter, producing
a deterministic 256-bit per-project key. This design means per-project
encryption keys are never stored on disk — they are re-derived from the
master PSK and project UUID on each archive or restore operation. If the
PSK is rotated, all existing projects become inaccessible, which is
documented as a deliberate security property rather than a bug.
The HKDF-Extract step uses the PSK as the salt with an empty initial key
material, producing a pseudorandom key. HKDF-Expand then derives 32 bytes
of output key material bound to the project UUID via the info parameter,
preventing cross-project key reuse even if an attacker learns one
project’s derived key.
Four tests verify deterministic derivation, UUID binding, PSK binding,
and correct 256-bit output length.
Implement complete integrity verification module with streaming hasher
Replace the stub hash_file function with a full implementation that reads
files in 64 KiB chunks through std::io::copy into a SHA-256 hasher,
avoiding memory pressure for arbitrarily large inputs. Add hash_bytes_raw
for callers that need the 32-byte binary digest rather than a hex string.
hash_reader accepts any io::Read source, enabling hash computation on
network streams or in-memory buffers without materializing intermediate
files.
Introduce StreamingHasher for incremental hash computation across
multiple update calls. This is designed for chunked network reception
where the full data is never in memory at once: the receiver calls update
on each chunk as it arrives, then finalize or finalize_raw to obtain the
digest. The struct wraps sha2::Sha256 and implements Default.
Verification helpers verify_hash and verify_file_hash compare computed
hashes against expected hex strings, returning bool rather than Result for
use in assert-style checks. The crypto module’s sha256_hex and sha256_file
functions are updated to delegate to the integrity module, making it the
single canonical location for SHA-256 operations.
Ten tests cover known-answer vectors for the empty string and ‘hello
world’, raw hash byte length, file hashing including a 10 MB large-file
path to exercise chunked I/O, hash verification for both match and
mismatch cases, file verification, streaming hasher correctness against a
known answer, streaming-vs-oneshot equivalence across four chunks, and
hash_reader with an in-memory source.
Add comprehensive protocol integration test suite
Introduce 20 integration tests in pwr-core/tests/protocol_integration.rs
that exercise the full protocol layer without network dependencies,
operating entirely on in-memory frame buffers and byte streams.
Frame encode/decode round-trips validate that ArchiveRequest,
RestoreRequest, and Handshake messages survive serialization through the
full pipeline: typed ClientMessage enum to JSON payload to framed bytes
with magic and length prefix through decode_frame back to typed enum with
correct field values.
FrameDecoder buffering tests cover two edge cases: byte-by-byte arrival
where the decoder correctly reports None for incomplete frames across
dozens of individual push_bytes calls until the full frame is available,
and multi-frame reads where two concatenated frames in a single buffer are
decoded sequentially without leftover bytes.
Error injection tests verify that bad magic bytes produce errors, truncated
frame headers (fewer than 10 bytes) return None rather than errors,
payloads exceeding the 16 MiB limit are rejected, unsupported protocol
versions are detected, and unknown message type bytes trigger framing
errors.
File chunk streaming tests cover exact 1 MiB chunks, zero-length data
(which is indistinguishable from the EOF marker by design since empty
chunks serve no purpose), EOF detection, and reassembly of 500 KB of
cyclical data across 4 KB chunks with content verification.
Serialization coverage iterates over all five ClientMessage variants and
all seven ServerMessage variants, verifying each round-trips through JSON
and preserves its message_type discriminant. Boundary tests handle empty
ProjectInfo lists, near-max-size payloads with u64::MAX size_bytes and
u32::MAX file_count, and direction-mismatch rejection where
decode_client_message and decode_server_message refuse payloads from the
wrong direction.
Refactor client to use typed ClientMessage/ServerMessage enums and protocol builders
Replace raw struct construction and manual frame encoding in the archive,
restore, and status methods with protocol builder functions and typed
message enums. archive_project now calls protocol::build_archive_request
and protocol::build_archive_complete. restore_project uses
protocol::build_restore_request. The send_client_msg and recv_server_msg
helpers replace the generic send_frame and recv_frame functions, providing
type-safe serialization for ClientMessage enums and typed deserialization
via protocol::decode_server_message for ServerMessage responses.
The handshake function is updated to construct the Handshake message as a
ClientMessage::Handshake variant and match the response as a
ServerMessage::HandshakeAck variant, with error handling for
ServerMessage::Error responses. Response parsing in all methods uses match
on ServerMessage variants instead of serde_json::from_slice with unwrapped
payloads, catching unexpected message types at the call site rather than
failing silently with deserialization errors.
The import list is reduced from a wildcard protocol::* to explicit imports
for ClientMessage, Handshake, ProjectInfo, ServerMessage, and the protocol
module itself for builder function access.
Refactor server handler to use ClientMessage/ServerMessage enums and protocol builders
Rewrite the connection handler’s dispatch function to decode raw frame
payloads through protocol::decode_client_message, which returns a typed
ClientMessage enum. The match statement now patterns on the enum variant
directly rather than matching MessageType discriminants and manually
deserializing individual structs. This eliminates redundant deserialization
code and catches direction mismatches at the protocol boundary.
All server response construction now uses the protocol builder functions:
build_handshake_ack_success, build_handshake_ack_failed, build_archive_accept,
build_restore_accept, build_status_response, and build_error. The
send_server_msg helper takes a ServerMessage enum, calls message_type() for
the frame header discriminant, and serializes through encode_frame. This
replaces scattered struct literal construction and manual frame encoding
with a single call site.
The handler functions (handle_handshake, handle_archive_start,
handle_archive_finish, handle_restore_start, handle_status) are unchanged
in logic but now operate on the inner payload structs extracted by the
dispatch layer. Storage locking uses scoped blocks with read().unwrap() and
write().unwrap() guards that drop before the next operation. Unused imports
are cleaned up, including the removal of the deprecated
ring::constant_time import.
Add typed builder functions for all protocol message flows
Introduce convenience constructors in the protocol module that build
ClientMessage and ServerMessage enums from their constituent fields,
eliminating the need for callers to construct nested struct literals.
For the archive flow, build_archive_request accepts a UUID, name, size,
file count, and compression flag, while build_archive_complete and
build_archive_failed produce the terminal ArchiveComplete variants.
For the restore flow, build_restore_request wraps a project UUID,
build_restore_accept bundles session metadata with size and hash,
build_restore_complete signals success, and build_restore_failed
attaches an error string. Handshake builders handle both success and
failure Ack messages with the server nonce, proof, and optional
reason. Additional helpers cover ArchiveAccept, StatusResponse, and
generic Error messages. These builders centralize protocol message
construction so handler and client code does not repeat struct
literals and reduces the surface area for missing required fields.
Add ClientMessage and ServerMessage sum types with typed deserialization dispatch
Introduce two unified enums to the protocol module that wrap all message
variants for type-safe routing in the connection handler state machine.
ClientMessage covers the five messages the client can send (Handshake,
ArchiveRequest, ArchiveComplete, RestoreRequest, StatusRequest) and
ServerMessage covers the six server responses (HandshakeAck, ArchiveAccept,
RestoreAccept, RestoreComplete, StatusResponse, Error). Each variant
delegates serialization to the inner struct via serde’s internally-tagged
enum representation using a ‘type’ discriminator field.
The message_type method on each enum maps variants to their MessageType
discriminant byte, enabling the frame encoder to write the correct header
without the caller tracking the mapping manually. Two deserialization
functions, decode_client_message and decode_server_message, take a raw
payload and a MessageType discriminant and return the typed enum. They
validate that the discriminant matches the expected direction, rejecting
client message types when a server message is expected and vice versa,
catching protocol-level dispatch bugs at the decoding layer rather than
deeper in handler logic.
The MessageType enum is relocated from the frame module to the protocol
module since it is a protocol concern, resolving a circular import between
the two modules. Frame encoding continues to reference MessageType for the
frame header type byte. The from_byte constructor maps all 13 assigned
discriminants with explicit match arms for stability.
Eight new tests cover ClientMessage and ServerMessage JSON round-trips,
direction-mismatch rejection in decode_client_message and
decode_server_message, and message_type discriminant mapping for both
enums, adding to the existing serialization and discriminant tests.
Add comprehensive README with architecture overview, security model, and setup guides
Document the complete pwr project covering the three-crate architecture,
security model with its three layers (TLS transport, PSK authentication,
and age at-rest encryption), quick-start guides for both Debian 12 server
and Arch Linux client, the .project.toml file format specification,
complete command reference table, configuration file reference for both
client and server TOML files, and build-from-source instructions with
feature flags for the optional TUI.
Add TLS certificate generation and server initialization utility
Implement a certificate generation module in pwr-server that produces
self-signed ECDSA P-256 certificates for TLS 1.3 via the rcgen crate.
generate_certificate accepts a common name (typically the server hostname),
creates certificate parameters with a distinguished name, generates a P-256
keypair, self-signs the certificate with a 365-day validity, and returns
the PEM-encoded certificate, PEM-encoded private key, and a SHA-256 hex
fingerprint of the certificate for client-side pinning.
save_certificate writes the certificate and key to the configured paths
with appropriate permissions: the certificate file is world-readable while
the private key is restricted to owner-only (mode 0o600 on Unix). Parent
directories are created automatically.
The init_server function ties everything together for first-time setup. It
generates the TLS certificate and key, saves them to /etc/pwr/, generates
a random 256-bit PSK via ring’s CSPRNG, creates a ServerConfig with the
PSK and certificate paths, and saves the config to /etc/pwr/server.toml.
The PSK hex string and certificate fingerprint are printed to stdout with
instructions for copying the PSK to the client config. This gives
administrators a single command to bootstrap a new pwr-server instance
with all security material generated fresh and properly permissioned.
Add ratatui-based TUI with project browser, creator, and log viewer screens
Implement a terminal UI for interactive project management using ratatui
0.28 with the crossterm backend. The application shell provides a tabbed
interface with three screens accessible via number keys (1-3) or Tab
cycling. A status bar at the bottom displays global keybindings.
The Project List screen renders a scrollable table of tracked projects
discovered by scanning the local root directory for .project.toml files.
Each row displays a color-coded status indicator (green for local, yellow
for archived), project name, human-readable size, and last-sync date.
Vim-style j/k and arrow key navigation is supported. Pressing r refreshes
the list from disk. The screen is empty with an appropriate message when
no projects are configured.
The Project Creator screen provides a two-field form for interactively
creating .project.toml files. The name field is pre-filled from the
current directory name. Tab and Shift+Tab cycle focus between name and
local path fields. Typing appends characters, backspace deletes. Ctrl+S
validates that the path exists and is a directory, that the name is
non-empty, then writes the .project.toml via the core library’s atomic
write function. Status messages appear on success or validation failure.
The Log Viewer screen reads the local JSONL transaction log and displays
entries in a scrollable table with timestamp, operation type, project
name, data size, and color-coded status (green OK, red FAILED, yellow
in-progress). Supports refresh with r and j/k navigation.
The TUI is enabled behind the tui Cargo feature flag which gates the
ratatui and crossterm dependencies.
Implement full CLI command suite with shell integration and client networking
Implement archive packaging pipeline and client networking module
Add the archive packaging pipeline to pwr-core. The create_archive function
builds an encrypted project archive through a four-stage streaming
pipeline. First, the project directory is walked recursively and added to a
tar archive via the tar crate, excluding .project.toml metadata files.
Second, the tar stream is gzip-compressed using flate2 at default
compression level and written to a temporary file to bound memory usage
independent of project size. Third, the compressed tarball is read back and
encrypted using the age X25519 public key from the project’s metadata,
producing an opaque encrypted blob the server cannot read. Fourth, the
SHA-256 hash of the encrypted blob is computed for integrity verification
after transfer. Symlinks are preserved in the archive. The reverse
pipeline, extract_archive, first verifies the SHA-256 hash against the
expected value, then age-decrypts, gunzips, and untars into the target
directory. Two tests verify create-extract round-trip fidelity
(README.md, src/main.rs, and Cargo.toml are restored with correct content)
and that hash mismatch prevents extraction.
Add the protocol client module to the pwr binary. PwrClient manages a
connection to pwr-server with TCP socket setup, configurable timeouts, PSK
handshake authentication with mutual server proof verification, and framed
message send/receive via the core frame module. The archive_project method
sends an ArchiveRequest, receives acceptance, streams archive data in 1 MiB
chunks with 4-byte length prefixes and a zero-length EOF marker, and sends
ArchiveComplete with the SHA-256 hash. The restore_project method requests
a project by UUID, receives a RestoreAccept with size metadata, streams
chunks back from the server, and returns the complete encrypted blob.
get_status queries the server for project listings with optional UUID
filtering. The handshake helper generates a 32-byte CSPRNG nonce, computes
the HMAC-SHA256 client proof, sends the Handshake message, verifies the
server’s HandshakeAck success flag, and performs mutual authentication by
validating the server proof against the expected value using constant-time
comparison.
Implement pwr-server storage backend, connection handler, and TLS listener
Add the three core server modules that together form the NAS-side daemon.
The storage module (storage.rs) manages an on-disk project registry as a
JSON index file mapping UUIDs to StoredProject entries containing name,
size, file count, encryption flag, and timestamps. The registry is
persisted atomically using a write-to-temp-then-rename pattern. Per-project
directories under the configured storage base path hold data.enc (the
encrypted archive blob) and meta.toml (per-project metadata). Methods
provide CRUD operations on the registry, streaming archive read/write
through buffered I/O, storage limit enforcement against the configured
max_project_size_gb, and filesystem directory management. Five tests verify
create-and-list, duplicate rejection, remove, archive write-read round-trip,
and registry persistence across re-opens.
The handler module (handler.rs) implements a per-connection state machine
with five states: AwaitingHandshake, Authenticated, Archiving, Restoring,
and Closed. The dispatch function routes decoded frames by state and
message type. The handshake handler validates the client’s HMAC-SHA256
proof in constant time using ring::constant_time::verify_slices_are_equal,
then generates a server nonce and server proof for mutual authentication.
Archive start creates a project entry after checking the storage limit,
archive finish updates the final size or rolls back on failure. Restore
start looks up the project and verifies the archive file exists. Status
queries support both single-UUID lookup and full listing. Storage is shared
across connections via Arc<RwLock> with per-operation
read or write locking.
The listener module (listener.rs) loads PEM-encoded TLS certificates via
rustls-pemfile, builds a rustls ServerConfig with TLS 1.3 and the
application-level protocol negotiation tag “pwr/1”, binds a std::net
TcpListener, and spawns each accepted connection on a dedicated OS thread
with a rustls::StreamOwned wrapping the TCP stream for TLS encryption.
Implement cryptographic layer with PSK authentication, age encryption, and SHA-256 integrity
Add the complete cryptographic subsystem to pwr-core, providing three
layers of security for the protocol. The pre-shared key authentication
layer uses HMAC-SHA256 to compute challenge-response proofs. The client
proves knowledge of the PSK by sending HMAC(client_nonce || “pwr-auth-v1”,
PSK). The server responds with HMAC(client_nonce || server_nonce ||
“pwr-auth-v1”, PSK), enabling mutual authentication. Both nonces are
32-byte CSPRNG values generated per-connection using ring’s SystemRandom.
The PSK itself is a 256-bit random key stored as a hex string in both
config files. Helper functions handle hex encoding and decoding.
At-rest encryption uses age with X25519 key pairs. generate_age_identity
creates a fresh keypair, stores the secret identity at
~/.config/pwr/identity with 0o600 permissions, and returns the Bech32
public key for inclusion in .project.toml. The age_encrypt function takes
a public key string and plaintext, producing an age-format encrypted blob
using the one-shot Encryptor API. age_decrypt reverses this with an
identity, returning the original plaintext. Both functions propagate
structured errors for wrong keys, corrupted data, and unsupported formats.
Integrity verification uses SHA-256 via the sha2 crate. sha256_hex
hashes an in-memory buffer. sha256_file streams through a file in 64 KiB
chunks to support large projects without memory pressure. Ten tests
verify PSK randomness and hex round-trips, deterministic proof
computation (same inputs produce same outputs), mutual proof divergence
(client and server proofs differ), age encrypt-decrypt round-trips for
both empty and non-empty payloads, wrong-identity rejection, known-answer
SHA-256 vectors, and file hashing against the same known answer.
Implement wire protocol message types and frame encoding layer
Define the complete set of protocol messages shared between client and
server in pwr-core. The MessageType enum assigns stable 1-byte
discriminants to each message variant for routing in the frame header.
Handshake messages carry 32-byte client and server nonces with
HMAC-SHA256 proofs for mutual PSK authentication, plus a server
version string and optional error reason for debugging failed handshakes.
Archive flow messages coordinate project uploads: ArchiveRequest
announces the project UUID, name, total size, file count, and compression
flag; ArchiveAccept returns a session UUID for correlating subsequent
chunks; ArchiveComplete reports the final SHA-256 hash and transfer size.
Restore flow mirrors this with RestoreRequest, RestoreAccept (carrying
size and hash so the client can pre-allocate), and RestoreComplete.
File streaming uses FileHeader (relative path, size, Unix mode bits) sent
before raw chunk data and FileEnd (SHA-256 checksum) sent after. Query
messages provide StatusRequest with optional project UUID filter and
StatusResponse with a ProjectInfo vector carrying UUID, name, size, file
count, and timestamps.
The frame module implements a 10-byte header format: 4 magic bytes
(0x50575246, ASCII “PWRF”), 4-byte big-endian payload length, 1-byte
protocol version, and 1-byte message type. Payloads are JSON-encoded for
debuggability. encode_frame serializes and wraps a message; decode_frame
validates magic, version, and length bounds, returning None when more
data is needed. FrameDecoder maintains an internal buffer for incremental
socket reads. File chunks use a separate 4-byte length-prefixed streaming
format with a zero-length EOF marker, keeping large file data out of the
JSON framing layer.
Thirteen tests cover message type discriminants, serialization round-trips
for ArchiveRequest and ProjectInfo, handshake nonce sizes, frame
encode-decode with magic verification, partial-read buffering in
FrameDecoder, oversized frame rejection, bad magic rejection, file chunk
encode-decode, and EOF marker detection.
Implement project file I/O with atomic writes, placeholder detection, and directory utilities
Add the complete project file management layer in pwr-core. The
.project.toml file is the stable identity marker that persists on local
disk regardless of whether project content is present (Local state) or
stored on the server (Archived state).
read_project_file returns Option, returning None when no
.project.toml exists so callers can distinguish untracked directories
from tracked projects. write_project_file uses an atomic write-through
pattern: contents are serialized to a .tmp sibling file first, then
renamed over the target, preventing readers from observing a partially
written metadata file. remove_project_file deletes the marker without
touching directory contents.
State detection predicates cover all cases needed by the CLI and shell
wrapper. is_archived_placeholder confirms that a directory contains only
a .project.toml whose state field is Archived — the signal that triggers
automatic restore in the ensure command. is_local_project checks for the
opposite state. is_tracked simply tests file existence without parsing.
Directory utilities support the archive and restore workflows.
dir_size computes total bytes excluding the metadata file itself, with
symlinks counted by their target path length rather than followed.
file_count tallies regular files recursively for transfer progress
estimation. remove_dir_contents_except_project strips a directory down
to its .project.toml placeholder after a successful archive.
Eleven tests cover round-trip I/O, atomic write behavior, placeholder
and local state detection, tracking predicate, exact byte counting, file
enumeration, directory stripping, and recursive discovery.
Implement server-side configuration management with validation and storage layout
Add the ServerConfig struct to the pwr-server crate, controlling all
aspects of the daemon’s operation. The configuration specifies the TCP
bind address and port (defaulting to 0.0.0.0:9742), the filesystem root
for project storage (defaulting to /srv/pwr/projects), paths to the TLS
certificate and private key files, the hex-encoded 256-bit pre-shared
authentication token that clients must present during the handshake, and
operational limits including maximum concurrent connections (default 32),
maximum project size in gigabytes (default 500), and an idle timeout for
authenticated connections (default 300 seconds).
Validation is performed at load time: the port must be non-zero, the auth
token must be non-empty, and both max_project_size_gb and max_connections
must be positive. The validate method returns a plain String error so the
caller can present it directly to the user without depending on the
shared pwr-core error types, keeping the server crate’s dependency
surface minimal during early development.
A config file search strategy checks four locations in order: an explicit
–config CLI path, the current working directory (for development), the
XDG config directory under ~/.config/pwr/, and the system-wide
/etc/pwr/server.toml. Helper methods compute derived paths:
project_dir returns the per-UUID storage subdirectory, registry_path
returns the JSON index file location, and bind_addr formats the socket
address for TcpListener::bind.
Seven tests cover default validation, empty-token rejection, zero-port
rejection, bind address formatting, project directory path generation,
registry path naming, and a full save-load round-trip through TOML
serialization and deserialization.
Add client configuration management with server connection settings
Refactor the PwrConfig struct to support the protocol-based architecture
by replacing rsync-oriented fields (NAS hostname, SSH user, rsync options)
with connection parameters for the pwr-server daemon. The new fields are
server_host for the NAS hostname or IP address, server_port defaulting to
9742, and server_psk storing a hex-encoded 256-bit pre-shared key for
HMAC-SHA256 authentication during the TLS handshake.
Certificate pinning is supported through an optional server_fingerprint
field that stores the SHA-256 hash of the server’s self-signed X.509
certificate. When set, the client rejects connections where the server
presents a certificate with a different fingerprint, preventing
man-in-the-middle attacks even if an attacker obtains a valid CA-signed
certificate for the server’s hostname.
Timeout configuration separates connection establishment (default 10
seconds) from long-running transfers (default 300 seconds) so users can
tune them independently. Path helpers provide the standard XDG config
directory layout: config.toml for connection settings, identity for the
age encryption keypair, and transactions.log for the JSONL audit trail.
The config_exists predicate lets callers check for prior initialization
without triggering the NoConfig error path. The load_config function maps
TOML parse errors into the TomlParse variant carrying the file path for
actionable error messages. A server_addr convenience method joins host
and port for use with TcpStream::connect. Three tests verify address
formatting, config path construction, and port default values.
The error module in pwr-core defines PwrError as a comprehensive thiserror
enum covering I/O failures, TOML and bincode serialization errors,
protocol framing violations, cryptographic operation failures,
authentication rejections, network connectivity issues, storage-layer
problems like missing or corrupted projects, and timeouts. Each variant
carries contextual information: the TOML parse error includes the file
path, protocol errors carry descriptive strings, and storage errors
distinguish between not-found, already-exists, and corrupted states.
Client configuration (PwrConfig) is rewritten with fields for server
hostname, port, hex-encoded pre-shared key, optional TLS certificate
fingerprint for pinning, local project root path, and configurable
connect and transfer timeouts. The old rsync-oriented fields (NAS host,
SSH user, rsync options) are removed. Server configuration is deferred to
the pwr-server crate for a later commit.
Stub modules for protocol message types, frame encoding, cryptographic
key management, archive packaging, and integrity verification are placed
in pwr-core so subsequent commits can fill them in without structural
changes. Both binaries compile as minimal stubs printing their version.
Ten tests pass covering config serialization, metadata state transitions,
project file round-trips, transaction log JSONL format, and SHA-256 hash
verification of known inputs.