KeiSeiKit-1.0/_primitives/_rust/kei-buddy/src/lib.rs
Parfii-bot 86834b82af feat(kei-buddy): provision_decrypt — VPS-side blob decryption
Mirrors keisei-marketplace/src/lib/crypto-box.ts::sealBoxToVps.

Two new subcommands on kei-buddy bin:
  - genkeys --key <path>          → writes PKCS#8 PEM x25519 priv,
                                    prints standard-base64 pub (44 char)
  - decrypt-and-export --vps-key <pem> --blob <json> --env-out <env>
                                  → ECDH(vps_priv, ephPub) → HKDF-SHA256
                                    info=keibuddy-token-v1 → XChaCha20-Poly1305
                                    decrypt → append BOT_TOKEN/TELEGRAM_BOT_TOKEN
                                    to env file (replaces stale, keeps other lines)

Cloud-init in hetzner.ts already calls these. Without this commit the
VPS could decode its own pubkey but had no way to recover the sealed
bot-token blob — the bot would never log into Telegram.

Crypto stack (mirror of @noble in TS):
  - x25519-dalek 2 (static_secrets feature)
  - chacha20poly1305 0.10 (XChaCha20Poly1305)
  - hkdf 0.12, sha2 0.10
  - base64 0.22 (accepts URL_SAFE_NO_PAD + STANDARD)
  - zeroize 1 for priv-key wipe

Tests (6/6 pass):
  - roundtrip_seal_then_decrypt — re-implement marketplace sealing in Rust,
    verify our decryption recovers plaintext
  - decrypt_and_export_writes_env_file — full e2e through CLI surface
  - decrypt_and_export_replaces_existing_token — stale BOT_TOKEN replaced,
    other env lines preserved
  - decrypt_rejects_wrong_key — XChaCha20 AEAD tag fails on wrong key
  - pem_roundtrip — write_pkcs8 + parse_pkcs8 round-trip
  - b64decode_accepts_urlsafe_and_standard — handles both encodings

Cross-verified end-to-end:
  $ node marketplace_seal.mjs <pub> <token>  →  /tmp/blob.json
  $ kei-buddy decrypt-and-export --vps-key ... → BOT_TOKEN matches input

Constructor Pattern: 1 file (provision_decrypt.rs, 344 LOC), 1 module,
1 responsibility (token-blob decryption + key generation).

=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
behaviour-verified: yes (e2e marketplace-seal → kei-buddy-decrypt round-trip)
follow-up-required:
  - none
2026-05-15 17:49:59 +08:00

60 lines
1.8 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
//! kei-buddy — KeiBuddy personal-assistant Telegram bot scaffold.
//!
//! Module layout (Constructor Pattern — one file, one responsibility):
//! * `state` — `OnboardState` enum
//! * `transition` — `StepOutput` output struct
//! * `extractor` — `LlmExtractor` trait + `MockExtractor` + `OpenAiExtractor` (feature-gated)
//! * `machine` — `handle_step` — the 11-arm onboarding FSM
//! * `error` — `BuddyError` error type
//! * `schema` — buddy-specific SQLite DDL
//! * `store` — `BuddyStore` trait + `SqliteBuddyStore` impl
pub mod chat_log;
pub(crate) mod command_exec;
pub mod commands;
pub mod contacts;
pub mod contacts_sync;
pub mod conversational;
pub mod error;
pub mod extractor;
pub mod machine;
pub(crate) mod machine_helpers;
pub(crate) mod machine_lang;
pub mod persona_merge;
pub mod provision_decrypt;
pub mod retrieval;
pub mod schema;
pub mod state;
pub mod store;
pub(crate) mod store_ops;
pub mod strings;
pub mod tick;
pub mod topic_classify;
pub mod topics;
pub mod transition;
#[cfg(feature = "serve")]
pub mod serve;
#[cfg(feature = "serve")]
pub(crate) mod serve_runner;
#[cfg(feature = "serve")]
pub mod serve_telegram;
#[cfg(feature = "serve")]
pub mod voice;
pub use chat_log::ChatLog;
pub use commands::{parse_command, execute_command, Command, CommandStores};
pub use contacts_sync::{sync_from_apple, sync_from_google, SyncReport};
pub use contacts::Contacts;
pub use error::BuddyError;
pub use extractor::LlmExtractor;
pub use machine::handle_step;
pub use state::OnboardState;
pub use store::{BuddyStore, SqliteBuddyStore};
pub use strings::{Lang, Strings};
pub use tick::{run_tick, run_tick_with, TickConfig, TickReport};
pub use topics::Topics;
pub use transition::StepOutput;
#[cfg(feature = "serve")]
pub use voice::VoiceHandler;